@newrelic/browser-agent 1.316.0 → 1.317.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 (170) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/config/configurable.js +1 -1
  3. package/dist/cjs/common/config/init-types.js +1 -0
  4. package/dist/cjs/common/config/init.js +6 -4
  5. package/dist/cjs/common/constants/env.cdn.js +2 -2
  6. package/dist/cjs/common/constants/env.npm.js +2 -2
  7. package/dist/cjs/common/constants/events.js +26 -0
  8. package/dist/cjs/common/harvest/harvester.js +9 -217
  9. package/dist/cjs/common/harvest/send.js +232 -0
  10. package/dist/cjs/common/harvest/types.js +9 -5
  11. package/dist/cjs/common/payloads/payloads.js +127 -0
  12. package/dist/cjs/common/serialize/bel-serializer.js +4 -3
  13. package/dist/cjs/common/session/session-entity.js +8 -8
  14. package/dist/cjs/common/util/data-size.js +2 -2
  15. package/dist/cjs/common/util/obfuscate.js +147 -3
  16. package/dist/cjs/common/v2/mfe-vitals.js +286 -0
  17. package/dist/cjs/common/wrap/wrap-websocket.js +2 -1
  18. package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
  19. package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
  20. package/dist/cjs/features/ajax/aggregate/index.js +51 -13
  21. package/dist/cjs/features/ajax/constants.js +6 -1
  22. package/dist/cjs/features/ajax/instrument/index.js +105 -17
  23. package/dist/cjs/features/generic_events/aggregate/index.js +15 -10
  24. package/dist/cjs/features/generic_events/constants.js +3 -2
  25. package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
  26. package/dist/cjs/features/jserrors/aggregate/index.js +12 -3
  27. package/dist/cjs/features/logging/aggregate/index.js +12 -8
  28. package/dist/cjs/features/metrics/aggregate/index.js +5 -1
  29. package/dist/cjs/features/page_view_event/aggregate/index.js +72 -33
  30. package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -1
  31. package/dist/cjs/features/session_replay/aggregate/index.js +39 -29
  32. package/dist/cjs/features/session_trace/aggregate/index.js +35 -29
  33. package/dist/cjs/features/session_trace/constants.js +17 -3
  34. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +36 -5
  35. package/dist/cjs/features/soft_navigations/aggregate/index.js +7 -1
  36. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  37. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +6 -4
  38. package/dist/cjs/features/utils/aggregate-base.js +4 -3
  39. package/dist/cjs/loaders/api/register.js +26 -4
  40. package/dist/esm/common/config/configurable.js +1 -1
  41. package/dist/esm/common/config/init-types.js +1 -0
  42. package/dist/esm/common/config/init.js +3 -1
  43. package/dist/esm/common/constants/env.cdn.js +2 -2
  44. package/dist/esm/common/constants/env.npm.js +2 -2
  45. package/dist/esm/common/constants/events.js +20 -0
  46. package/dist/esm/common/harvest/harvester.js +9 -217
  47. package/dist/esm/common/harvest/send.js +226 -0
  48. package/dist/esm/common/harvest/types.js +9 -5
  49. package/dist/esm/common/payloads/payloads.js +118 -0
  50. package/dist/esm/common/serialize/bel-serializer.js +4 -3
  51. package/dist/esm/common/session/session-entity.js +9 -9
  52. package/dist/esm/common/util/data-size.js +2 -2
  53. package/dist/esm/common/util/obfuscate.js +147 -3
  54. package/dist/esm/common/v2/mfe-vitals.js +281 -0
  55. package/dist/esm/common/wrap/wrap-websocket.js +2 -1
  56. package/dist/esm/common/wrap/wrap-xhr.js +1 -1
  57. package/dist/esm/features/ajax/aggregate/gql.js +39 -1
  58. package/dist/esm/features/ajax/aggregate/index.js +52 -14
  59. package/dist/esm/features/ajax/constants.js +5 -0
  60. package/dist/esm/features/ajax/instrument/index.js +106 -18
  61. package/dist/esm/features/generic_events/aggregate/index.js +15 -10
  62. package/dist/esm/features/generic_events/constants.js +3 -2
  63. package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
  64. package/dist/esm/features/jserrors/aggregate/index.js +12 -3
  65. package/dist/esm/features/logging/aggregate/index.js +12 -8
  66. package/dist/esm/features/metrics/aggregate/index.js +5 -1
  67. package/dist/esm/features/page_view_event/aggregate/index.js +71 -32
  68. package/dist/esm/features/page_view_timing/aggregate/index.js +6 -1
  69. package/dist/esm/features/session_replay/aggregate/index.js +39 -29
  70. package/dist/esm/features/session_trace/aggregate/index.js +36 -30
  71. package/dist/esm/features/session_trace/constants.js +16 -2
  72. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +36 -5
  73. package/dist/esm/features/soft_navigations/aggregate/index.js +7 -1
  74. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  75. package/dist/esm/features/soft_navigations/aggregate/interaction.js +6 -4
  76. package/dist/esm/features/utils/aggregate-base.js +4 -3
  77. package/dist/esm/loaders/api/register.js +26 -4
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/dist/types/common/config/init-types.d.ts +4 -0
  80. package/dist/types/common/config/init-types.d.ts.map +1 -1
  81. package/dist/types/common/config/init.d.ts.map +1 -1
  82. package/dist/types/common/constants/events.d.ts +16 -0
  83. package/dist/types/common/constants/events.d.ts.map +1 -0
  84. package/dist/types/common/harvest/harvester.d.ts +2 -16
  85. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  86. package/dist/types/common/harvest/send.d.ts +9 -0
  87. package/dist/types/common/harvest/send.d.ts.map +1 -0
  88. package/dist/types/common/harvest/types.d.ts +39 -10
  89. package/dist/types/common/harvest/types.d.ts.map +1 -1
  90. package/dist/types/common/payloads/payloads.d.ts +42 -0
  91. package/dist/types/common/payloads/payloads.d.ts.map +1 -0
  92. package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
  93. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  94. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  95. package/dist/types/common/util/obfuscate.d.ts +23 -2
  96. package/dist/types/common/util/obfuscate.d.ts.map +1 -1
  97. package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
  98. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
  99. package/dist/types/common/wrap/wrap-websocket.d.ts.map +1 -1
  100. package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
  101. package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
  102. package/dist/types/features/ajax/aggregate/index.d.ts +2 -0
  103. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/ajax/constants.d.ts +5 -0
  105. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  106. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  107. package/dist/types/features/generic_events/aggregate/index.d.ts +3 -1
  108. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  109. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  110. package/dist/types/features/jserrors/aggregate/index.d.ts +8 -1
  111. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  112. package/dist/types/features/logging/aggregate/index.d.ts +3 -1
  113. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  114. package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
  115. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  116. package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -0
  117. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  118. package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -0
  119. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  120. package/dist/types/features/session_replay/aggregate/index.d.ts +4 -2
  121. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  122. package/dist/types/features/session_trace/aggregate/index.d.ts +3 -1
  123. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  124. package/dist/types/features/session_trace/constants.d.ts +17 -0
  125. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +6 -1
  126. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  127. package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -0
  128. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  129. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -1
  130. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  131. package/dist/types/features/utils/aggregate-base.d.ts +0 -1
  132. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  133. package/dist/types/loaders/api/register.d.ts.map +1 -1
  134. package/package.json +3 -3
  135. package/src/common/config/configurable.js +1 -1
  136. package/src/common/config/init-types.js +1 -0
  137. package/src/common/config/init.js +2 -1
  138. package/src/common/constants/events.js +20 -0
  139. package/src/common/harvest/harvester.js +9 -201
  140. package/src/common/harvest/send.js +209 -0
  141. package/src/common/harvest/types.js +9 -5
  142. package/src/common/payloads/payloads.js +135 -0
  143. package/src/common/serialize/bel-serializer.js +4 -3
  144. package/src/common/session/session-entity.js +8 -9
  145. package/src/common/util/data-size.js +2 -2
  146. package/src/common/util/obfuscate.js +154 -3
  147. package/src/common/v2/mfe-vitals.js +260 -0
  148. package/src/common/wrap/wrap-websocket.js +2 -1
  149. package/src/common/wrap/wrap-xhr.js +1 -1
  150. package/src/features/ajax/aggregate/gql.js +42 -1
  151. package/src/features/ajax/aggregate/index.js +43 -13
  152. package/src/features/ajax/constants.js +5 -1
  153. package/src/features/ajax/instrument/index.js +114 -18
  154. package/src/features/generic_events/aggregate/index.js +14 -9
  155. package/src/features/generic_events/constants.js +3 -2
  156. package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
  157. package/src/features/jserrors/aggregate/index.js +12 -3
  158. package/src/features/logging/aggregate/index.js +12 -11
  159. package/src/features/metrics/aggregate/index.js +5 -1
  160. package/src/features/page_view_event/aggregate/index.js +58 -29
  161. package/src/features/page_view_timing/aggregate/index.js +6 -1
  162. package/src/features/session_replay/aggregate/index.js +37 -28
  163. package/src/features/session_trace/aggregate/index.js +37 -30
  164. package/src/features/session_trace/constants.js +7 -1
  165. package/src/features/soft_navigations/aggregate/ajax-node.js +26 -5
  166. package/src/features/soft_navigations/aggregate/index.js +7 -1
  167. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  168. package/src/features/soft_navigations/aggregate/interaction.js +6 -4
  169. package/src/features/utils/aggregate-base.js +3 -3
  170. package/src/loaders/api/register.js +24 -4
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { globalScope, isBrowserScope } from '../constants/runtime';
7
+ import { now } from '../timing/now';
8
+ import { cleanURL } from '../url/clean-url';
9
+
10
+ /**
11
+ * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
12
+ */
13
+
14
+ /**
15
+ * Generate a simple CSS selector for an element
16
+ * @param {Element} elem - The element to generate selector for
17
+ * @returns {string|null} CSS selector or null if element is invalid
18
+ */
19
+ const getElementSelector = elem => {
20
+ try {
21
+ if (!elem || !elem.tagName) return null;
22
+ let selector = elem.tagName.toLowerCase();
23
+ if (elem.id) selector += "#".concat(elem.id);else if (elem.className && typeof elem.className === 'string') {
24
+ const classes = elem.className.trim().split(/\s+/).slice(0, 2).join('.');
25
+ if (classes) selector += ".".concat(classes);
26
+ }
27
+ return selector;
28
+ } catch (e) {
29
+ return null;
30
+ }
31
+ };
32
+
33
+ /**
34
+ * Extract URL from an element (for images, videos, etc.)
35
+ * @param {Element} elem - The element to extract URL from
36
+ * @returns {string|null} Cleaned URL or null
37
+ */
38
+ const getElementURL = elem => {
39
+ try {
40
+ if (!elem) return null;
41
+
42
+ // For images and videos
43
+ if (elem.src) return cleanURL(elem.src);
44
+
45
+ // For elements with background images
46
+ const bgImage = globalScope.getComputedStyle?.(elem)?.backgroundImage;
47
+ if (bgImage && bgImage !== 'none') {
48
+ const match = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
49
+ if (match?.[1]) return cleanURL(match[1]);
50
+ }
51
+ return null;
52
+ } catch (e) {
53
+ return null;
54
+ }
55
+ };
56
+ const isObservable = node => {
57
+ try {
58
+ return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase());
59
+ } catch (e) {
60
+ return false;
61
+ }
62
+ };
63
+
64
+ /**
65
+ * Check if node is within a specific MFE
66
+ * @param {Node} node - DOM node to check
67
+ * @param {string} id - MFE ID to match
68
+ * @returns {boolean}
69
+ */
70
+ const isInMFE = (node, id) => {
71
+ try {
72
+ let curr = node.nodeType === 1 ? node : node.parentElement;
73
+ while (curr?.tagName) {
74
+ if (curr.dataset?.nrMfeId === id) return true;
75
+ curr = curr.parentNode;
76
+ }
77
+ } catch (e) {}
78
+ return false;
79
+ };
80
+
81
+ /**
82
+ * Create mutation observer for MFE nodes
83
+ * @param {string} id - MFE ID to track
84
+ * @param {Function} onMatch - Callback when matching node is added
85
+ * @returns {MutationObserver}
86
+ */
87
+ const observeMutations = (id, onMatch) => {
88
+ const obs = new globalScope.MutationObserver(mutations => {
89
+ mutations.forEach(m => m.addedNodes.forEach(node => {
90
+ if (isObservable(node) && isInMFE(node, id)) {
91
+ onMatch(node, obs);
92
+ }
93
+ }));
94
+ });
95
+ obs.observe(globalScope.document, {
96
+ childList: true,
97
+ subtree: true
98
+ });
99
+ return obs;
100
+ };
101
+
102
+ /**
103
+ * Create performance observer for MFE entries
104
+ * @param {Array} observers - Array to track observers
105
+ * @param {Object} config - Observer configuration
106
+ * @param {Function} onEntry - Callback for each entry
107
+ * @returns {PerformanceObserver|null} Observer if successful, null if unsupported
108
+ */
109
+ const observePerformance = (observers, config, onEntry) => {
110
+ try {
111
+ const obs = new globalScope.PerformanceObserver(list => {
112
+ list.getEntries().forEach(onEntry);
113
+ });
114
+ obs.observe(config);
115
+ observers.push(obs);
116
+ return obs;
117
+ } catch (e) {
118
+ return null;
119
+ }
120
+ };
121
+
122
+ /**
123
+ * Tracks all Core Web Vitals for a specific MFE.
124
+ * @param {string} id - The MFE ID to track
125
+ * @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
126
+ */
127
+ export function trackMFEVitals(id) {
128
+ const vitals = {
129
+ fcp: null,
130
+ lcp: null,
131
+ cls: null,
132
+ inp: null,
133
+ disconnect: () => {}
134
+ };
135
+ if (!id || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
136
+ const observers = [];
137
+
138
+ // Track FCP - first contentful paint
139
+ observeMutations(id, (node, obs) => {
140
+ if (vitals.fcp === null) {
141
+ vitals.fcp = {
142
+ value: now(),
143
+ loadState: globalScope.document?.readyState || null
144
+ };
145
+ obs.disconnect();
146
+ }
147
+ });
148
+
149
+ // Track LCP - largest contentful paint
150
+ let largestSize = 0;
151
+ const lcpObs = observeMutations(id, node => {
152
+ try {
153
+ const elem = node.nodeType === 1 ? node : node.parentElement;
154
+ if (!elem) return;
155
+ const rect = elem.getBoundingClientRect();
156
+ const size = rect.width * rect.height;
157
+ if (size > largestSize) {
158
+ largestSize = size;
159
+ vitals.lcp = {
160
+ value: now(),
161
+ size,
162
+ elTag: elem.tagName || null,
163
+ eid: elem.id || null,
164
+ elUrl: getElementURL(elem)
165
+ };
166
+ }
167
+ } catch (e) {
168
+ // Element may be detached from DOM
169
+ }
170
+ });
171
+ observers.push(lcpObs);
172
+
173
+ // Track CLS - cumulative layout shift
174
+ // Initialize CLS to 0 if browser supports it AND the MFE container exists in the DOM
175
+ let clsValue = 0;
176
+ const clsObs = observePerformance(observers, {
177
+ type: 'layout-shift',
178
+ buffered: true
179
+ }, entry => {
180
+ if (entry.hadRecentInput || !vitals.cls) return;
181
+ (entry.sources || []).some(source => {
182
+ if (isInMFE(source.node, id)) {
183
+ clsValue += entry.value;
184
+ vitals.cls ??= {
185
+ value: 0,
186
+ largestShiftValue: null,
187
+ largestShiftTime: null,
188
+ largestShiftTarget: null,
189
+ loadState: null
190
+ };
191
+ vitals.cls.value = clsValue;
192
+
193
+ // Track largest shift
194
+ if (entry.value > (vitals.cls.largestShiftValue || 0)) {
195
+ vitals.cls.largestShiftValue = entry.value;
196
+ vitals.cls.largestShiftTime = entry.startTime;
197
+ vitals.cls.largestShiftTarget = getElementSelector(source.node);
198
+ vitals.cls.loadState = globalScope.document?.readyState || null;
199
+ }
200
+ return true;
201
+ }
202
+ return false;
203
+ });
204
+ });
205
+ if (clsObs) {
206
+ try {
207
+ const mfeContainer = globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
208
+ if (mfeContainer) {
209
+ vitals.cls ??= {
210
+ value: 0,
211
+ largestShiftValue: null,
212
+ largestShiftTime: null,
213
+ largestShiftTarget: null,
214
+ loadState: null
215
+ };
216
+ }
217
+ } catch (e) {
218
+ // querySelector may fail, leave CLS as null
219
+ }
220
+ }
221
+
222
+ // Track INP - interaction to next paint
223
+ observePerformance(observers, {
224
+ type: 'event',
225
+ buffered: true,
226
+ durationThreshold: 16
227
+ }, entry => {
228
+ if (!entry.interactionId || !entry.target || !isInMFE(entry.target, id)) return;
229
+ if (vitals.inp === null || entry.duration > vitals.inp.value) {
230
+ vitals.inp = {
231
+ value: entry.duration,
232
+ interactionTarget: getElementSelector(entry.target),
233
+ interactionTime: entry.startTime,
234
+ interactionType: entry.name,
235
+ inputDelay: entry.processingStart ? entry.processingStart - entry.startTime : null,
236
+ processingDuration: entry.processingStart && entry.processingEnd ? entry.processingEnd - entry.processingStart : null,
237
+ presentationDelay: entry.processingEnd && entry.duration ? entry.duration - (entry.processingEnd - entry.startTime) : null,
238
+ nextPaintTime: entry.startTime + entry.duration,
239
+ loadState: globalScope.document?.readyState || null
240
+ };
241
+ }
242
+ });
243
+
244
+ // Disconnect all observers
245
+ vitals.disconnect = () => {
246
+ // Disconnect all observers
247
+ observers.forEach(obs => {
248
+ try {
249
+ obs?.disconnect();
250
+ } catch (e) {
251
+ // Observer may already be disconnected
252
+ }
253
+ });
254
+ };
255
+
256
+ // Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
257
+ // CLS and INP continue tracking until visibility change or deregister
258
+ const disconnectLCP = () => {
259
+ try {
260
+ lcpObs?.disconnect();
261
+ } catch (e) {
262
+ // Observer may already be disconnected
263
+ }
264
+ };
265
+ ['click', 'keydown', 'scroll'].forEach(type => {
266
+ globalScope.addEventListener(type, disconnectLCP, {
267
+ once: true,
268
+ passive: true
269
+ });
270
+ })
271
+
272
+ // Disconnect all observers on visibility change or page unload
273
+ ;
274
+ ['visibilitychange', 'pagehide'].forEach(type => {
275
+ globalScope.addEventListener(type, vitals.disconnect, {
276
+ once: true,
277
+ passive: true
278
+ });
279
+ });
280
+ return vitals;
281
+ }
@@ -8,6 +8,7 @@ import { now } from '../timing/now';
8
8
  import { cleanURL } from '../url/clean-url';
9
9
  import { gosNREUMOriginals } from '../window/nreum';
10
10
  import { subscribeToPageUnload } from '../window/page-visibility';
11
+ import { EVENT_TYPES } from '../constants/events';
11
12
  const wrapped = {};
12
13
  const openWebSockets = new Set(); // track all instances to close out metrics on page unload
13
14
 
@@ -33,7 +34,7 @@ export function wrapWebSocket(sharedEE) {
33
34
  });
34
35
  });
35
36
  class WrappedWebSocket extends WebSocket {
36
- static name = 'WebSocket';
37
+ static name = EVENT_TYPES.WS;
37
38
  static toString() {
38
39
  // fake native WebSocket when static class is stringified
39
40
  return 'function WebSocket() { [native code] }';
@@ -16,7 +16,7 @@ import { globalScope } from '../constants/runtime';
16
16
  import { warn } from '../util/console';
17
17
  import { findTargetsFromStackTrace } from '../v2/utils';
18
18
  const wrapped = {};
19
- const XHR_PROPS = ['open', 'send']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
19
+ const XHR_PROPS = ['open', 'send', 'setRequestHeader']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
20
20
 
21
21
  /**
22
22
  * Wraps the native XMLHttpRequest (XHR) object to emit custom events to its readystatechange event and an assortment
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { isPureObject } from '../../../common/util/type-check';
@@ -89,4 +89,42 @@ function parseGQLQueryString(gqlQueryString) {
89
89
  }
90
90
  function validateGQLObject(obj) {
91
91
  return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string');
92
+ }
93
+
94
+ /**
95
+ * Checks if a response object has valid GraphQL errors.
96
+ * @param {object} response - A single GraphQL response object
97
+ * @returns {boolean} True if the response has valid errors
98
+ */
99
+ function hasValidGQLErrors(response) {
100
+ return Array.isArray(response?.errors) && response.errors.some(err => err && typeof err === 'object' && typeof err.message === 'string');
101
+ }
102
+
103
+ /**
104
+ * Checks if a response body contains GraphQL errors according to the GraphQL spec.
105
+ * A valid GraphQL error response contains an "errors" array with at least one error object.
106
+ * Supports both single and batched GraphQL responses.
107
+ * @param {string|object|array} [responseBody] The response body to check
108
+ * @returns {boolean} True if the response contains GraphQL errors
109
+ */
110
+ export function hasGQLErrors(responseBody) {
111
+ if (!responseBody) return false;
112
+ try {
113
+ let parsed = responseBody;
114
+
115
+ // Parse string to object if needed
116
+ if (typeof responseBody === 'string') {
117
+ parsed = JSON.parse(responseBody);
118
+ }
119
+
120
+ // Handle batched GraphQL responses (array of response objects)
121
+ if (Array.isArray(parsed)) {
122
+ return parsed.some(hasValidGQLErrors);
123
+ }
124
+
125
+ // Handle single GraphQL response
126
+ return hasValidGQLErrors(parsed);
127
+ } catch (err) {
128
+ return false;
129
+ }
92
130
  }
@@ -6,13 +6,16 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
6
6
  import { stringify } from '../../../common/util/stringify';
7
7
  import { handle } from '../../../common/event-emitter/handle';
8
8
  import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list';
9
- import { AJAX_ID, FEATURE_NAME } from '../constants';
9
+ import { FEATURE_NAME, AJAX_ID } from '../constants';
10
10
  import { FEATURE_NAMES } from '../../../loaders/features/features';
11
11
  import { AggregateBase } from '../../utils/aggregate-base';
12
- import { parseGQL } from './gql';
13
12
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer';
14
13
  import { gosNREUMOriginals } from '../../../common/window/nreum';
14
+ import { hasGQLErrors, parseGQL } from './gql';
15
+ import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStringAdders } from '../../../common/payloads/payloads';
16
+ import { Obfuscator } from '../../../common/util/obfuscate';
15
17
  import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils';
18
+ import { EVENT_TYPES } from '../../../common/constants/events';
16
19
  import { generateUuid } from '../../../common/ids/unique-id';
17
20
  export class Aggregate extends AggregateBase {
18
21
  static featureName = FEATURE_NAME;
@@ -20,6 +23,9 @@ export class Aggregate extends AggregateBase {
20
23
  super(agentRef, FEATURE_NAME);
21
24
  setDenyList(agentRef.runtime.denyList);
22
25
  const classThis = this;
26
+
27
+ // Create obfuscator for AJAX requests
28
+ this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX);
23
29
  if (!agentRef.init.ajax.block_internal) {
24
30
  // if the agent is tracking ITSELF, it can spawn endless ajax requests early if they are large from custom attributes, so we just disable early harvest for ajax in this case.
25
31
  super.canHarvestEarly = false;
@@ -84,17 +90,26 @@ export class Aggregate extends AggregateBase {
84
90
  callbackDuration: metrics.cbTime,
85
91
  [AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
86
92
  };
93
+ event.gql = params.gql = parseGQL({
94
+ body: ctx.requestBody,
95
+ query: ctx.parsedOrigin?.search
96
+ });
97
+ if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = hasGQLErrors(ctx.responseBody);
98
+ const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads;
99
+ const shouldCapturePayload = canCapturePayload(capturePayloadSetting, params.status, event.gql?.operationHasErrors);
100
+ if (shouldCapturePayload) {
101
+ // 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;
107
+ }
87
108
  if (ctx.dt) {
88
109
  event.spanId = ctx.dt.spanId;
89
110
  event.traceId = ctx.dt.traceId;
90
111
  event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
91
112
  }
92
-
93
- // parsed from the AJAX body, looking for operationName param & parsing query for operationType
94
- event.gql = params.gql = parseGQL({
95
- body: ctx.body,
96
- query: ctx.parsedOrigin?.search
97
- });
98
113
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length);
99
114
 
100
115
  /** make a copy of the event for the MFE target if it exists */
@@ -122,7 +137,10 @@ export class Aggregate extends AggregateBase {
122
137
  }
123
138
  serializer(eventBuffer) {
124
139
  if (!eventBuffer.length) return;
125
- const addString = getAddStringContext(this.agentRef.runtime.obfuscator);
140
+ const {
141
+ addString,
142
+ addStringWithTruncation
143
+ } = createStringAdders(getAddStringContext, this.obfuscator);
126
144
  let payload = 'bel.7;';
127
145
  let firstTimestamp = 0;
128
146
  for (let i = 0; i < eventBuffer.length; i++) {
@@ -147,15 +165,35 @@ export class Aggregate extends AggregateBase {
147
165
  // Since configuration objects (like info) are created new each time they are set, we have to grab the current pointer to the attr object here.
148
166
  const jsAttributes = this.agentRef.info.jsAttributes;
149
167
 
150
- // add custom attributes
151
- // gql decorators are added as custom attributes to alleviate need for new BEL schema
152
- const attrParts = addCustomAttributes({
168
+ // Regular attributes: obfuscate only
169
+ const regularAttrs = addCustomAttributes({
153
170
  ...(jsAttributes || {}),
154
- ...(event.gql || {}),
171
+ [AJAX_ID]: event[AJAX_ID],
172
+ // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
155
173
  ...(event.targetAttributes || {}),
156
174
  // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
157
- [AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
175
+ ...(event.gql || {})
158
176
  }, addString);
177
+
178
+ // Payload attributes: obfuscate then truncate
179
+ const payloadAttrs = addCustomAttributes({
180
+ ...(event.requestBody ? {
181
+ requestBody: event.requestBody
182
+ } : {}),
183
+ ...(event.requestHeaders ? {
184
+ requestHeaders: event.requestHeaders
185
+ } : {}),
186
+ ...(event.requestQuery ? {
187
+ requestQuery: event.requestQuery
188
+ } : {}),
189
+ ...(event.responseBody ? {
190
+ responseBody: event.responseBody
191
+ } : {}),
192
+ ...(event.responseHeaders ? {
193
+ responseHeaders: event.responseHeaders
194
+ } : {})
195
+ }, addStringWithTruncation);
196
+ const attrParts = [...regularAttrs, ...payloadAttrs];
159
197
  fields.unshift(numeric(attrParts.length));
160
198
  insert += fields.join(',');
161
199
  if (attrParts && attrParts.length > 0) {
@@ -4,4 +4,9 @@
4
4
  */
5
5
  import { FEATURE_NAMES } from '../../loaders/features/features';
6
6
  export const FEATURE_NAME = FEATURE_NAMES.ajax;
7
+ export const CAPTURE_PAYLOAD_SETTINGS = {
8
+ NONE: 'none',
9
+ FAILURES: 'failures',
10
+ ALL: 'all'
11
+ };
7
12
  export const AJAX_ID = 'ajaxRequest.id';