@newrelic/browser-agent 1.316.0-rc.6 → 1.317.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 (64) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/config/init-types.js +1 -0
  3. package/dist/cjs/common/config/init.js +6 -4
  4. package/dist/cjs/common/constants/env.cdn.js +1 -1
  5. package/dist/cjs/common/constants/env.npm.js +1 -1
  6. package/dist/cjs/common/payloads/payloads.js +127 -0
  7. package/dist/cjs/common/serialize/bel-serializer.js +2 -1
  8. package/dist/cjs/common/util/data-size.js +2 -2
  9. package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
  10. package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
  11. package/dist/cjs/features/ajax/aggregate/index.js +46 -13
  12. package/dist/cjs/features/ajax/constants.js +6 -1
  13. package/dist/cjs/features/ajax/instrument/index.js +105 -17
  14. package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
  15. package/dist/cjs/features/jserrors/aggregate/index.js +1 -1
  16. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +35 -4
  17. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  18. package/dist/esm/common/config/init-types.js +1 -0
  19. package/dist/esm/common/config/init.js +3 -1
  20. package/dist/esm/common/constants/env.cdn.js +1 -1
  21. package/dist/esm/common/constants/env.npm.js +1 -1
  22. package/dist/esm/common/payloads/payloads.js +118 -0
  23. package/dist/esm/common/serialize/bel-serializer.js +2 -1
  24. package/dist/esm/common/util/data-size.js +2 -2
  25. package/dist/esm/common/wrap/wrap-xhr.js +1 -1
  26. package/dist/esm/features/ajax/aggregate/gql.js +39 -1
  27. package/dist/esm/features/ajax/aggregate/index.js +47 -14
  28. package/dist/esm/features/ajax/constants.js +5 -0
  29. package/dist/esm/features/ajax/instrument/index.js +106 -18
  30. package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
  31. package/dist/esm/features/jserrors/aggregate/index.js +1 -1
  32. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +35 -4
  33. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/dist/types/common/config/init-types.d.ts +4 -0
  36. package/dist/types/common/config/init-types.d.ts.map +1 -1
  37. package/dist/types/common/config/init.d.ts.map +1 -1
  38. package/dist/types/common/payloads/payloads.d.ts +42 -0
  39. package/dist/types/common/payloads/payloads.d.ts.map +1 -0
  40. package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
  41. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  42. package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
  43. package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
  44. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  45. package/dist/types/features/ajax/constants.d.ts +5 -0
  46. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  47. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  48. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +5 -0
  49. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  50. package/package.json +2 -2
  51. package/src/common/config/init-types.js +1 -0
  52. package/src/common/config/init.js +2 -1
  53. package/src/common/payloads/payloads.js +135 -0
  54. package/src/common/serialize/bel-serializer.js +2 -1
  55. package/src/common/util/data-size.js +2 -2
  56. package/src/common/wrap/wrap-xhr.js +1 -1
  57. package/src/features/ajax/aggregate/gql.js +42 -1
  58. package/src/features/ajax/aggregate/index.js +38 -13
  59. package/src/features/ajax/constants.js +5 -1
  60. package/src/features/ajax/instrument/index.js +114 -18
  61. package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
  62. package/src/features/jserrors/aggregate/index.js +1 -1
  63. package/src/features/soft_navigations/aggregate/ajax-node.js +25 -4
  64. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
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.317.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.316.0...v1.317.0) (2026-06-18)
7
+
8
+
9
+ ### Features
10
+
11
+ * Detect and report AJAX payloads ([#1651](https://github.com/newrelic/newrelic-browser-agent/issues/1651)) ([08394db](https://github.com/newrelic/newrelic-browser-agent/commit/08394db8a5a9f2b6fd627e9eee76022977cb433c))
12
+ * Enhance obfuscation to support event filter ([#1782](https://github.com/newrelic/newrelic-browser-agent/issues/1782)) ([c97aaf9](https://github.com/newrelic/newrelic-browser-agent/commit/c97aaf9773cfb0b62a810eb07b37e6f5b96d5ab5))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * Calculate browser_stack_hash from truncated stack trace ([#1789](https://github.com/newrelic/newrelic-browser-agent/issues/1789)) ([f7df627](https://github.com/newrelic/newrelic-browser-agent/commit/f7df627b14e3e5e8fa4a8d94433ea76e9a9902bb))
18
+ * Get responseBodySize from body if content-length is missing ([#1793](https://github.com/newrelic/newrelic-browser-agent/issues/1793)) ([58663ec](https://github.com/newrelic/newrelic-browser-agent/commit/58663ec19dc99ec94e34c17a5dfaa20a07038dba))
19
+ * Patch handling of inlining images for Session Replay ([#1765](https://github.com/newrelic/newrelic-browser-agent/issues/1765)) ([b330f8e](https://github.com/newrelic/newrelic-browser-agent/commit/b330f8e3e35f2fda076967092bcb70e792de6eeb))
20
+
6
21
  ## [1.316.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.315.0...v1.316.0) (2026-06-01)
7
22
 
8
23
 
@@ -16,6 +16,7 @@ exports.default = void 0;
16
16
  * @property {boolean} [ajax.block_internal] - If true, agent requests going to harvest endpoint are treated as on deny list. In other words, agent will not self-report AJAX.
17
17
  * @property {boolean} [ajax.enabled] - Turn on/off the ajax feature (on by default).
18
18
  * @property {boolean} [ajax.autoStart] - If true, the agent will automatically start the ajax feature. Otherwise, it will be in a deferred state until the `start` API method is called.
19
+ * @property {('none'|'failures'|'all')} [ajax.capture_payloads] - Controls when AJAX request/response payloads are captured. 'none' = never capture, 'failures' = capture only on errors (4xx, 5xx, network errors, GraphQL errors), 'all' = always capture.
19
20
  * @property {Object} [api]
20
21
  * @property {Object} [api.register]
21
22
  * @property {boolean} [api.register.enabled] - If true, the agent will allow registered children to be sent to the server.
@@ -5,8 +5,9 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.mergeInit = void 0;
7
7
  var _constants = require("../../features/generic_events/constants");
8
+ var _constants2 = require("../../features/ajax/constants");
8
9
  var _querySelector = require("../dom/query-selector");
9
- var _constants2 = require("../session/constants");
10
+ var _constants3 = require("../session/constants");
10
11
  var _console = require("../util/console");
11
12
  var _configurable = require("./configurable");
12
13
  /**
@@ -57,7 +58,8 @@ const InitModelFn = () => {
57
58
  deny_list: undefined,
58
59
  block_internal: true,
59
60
  enabled: true,
60
- autoStart: true
61
+ autoStart: true,
62
+ capture_payloads: _constants2.CAPTURE_PAYLOAD_SETTINGS.NONE
61
63
  },
62
64
  api: {
63
65
  register: {
@@ -141,8 +143,8 @@ const InitModelFn = () => {
141
143
  beacon: undefined
142
144
  },
143
145
  session: {
144
- expiresMs: _constants2.DEFAULT_EXPIRES_MS,
145
- inactiveMs: _constants2.DEFAULT_INACTIVE_MS
146
+ expiresMs: _constants3.DEFAULT_EXPIRES_MS,
147
+ inactiveMs: _constants3.DEFAULT_INACTIVE_MS
146
148
  },
147
149
  session_replay: {
148
150
  autoStart: true,
@@ -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.316.0-rc.6";
20
+ const VERSION = exports.VERSION = "1.317.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.316.0-rc.6";
20
+ const VERSION = exports.VERSION = "1.317.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.canCapturePayload = canCapturePayload;
7
+ exports.createStringAdders = createStringAdders;
8
+ exports.isLikelyHumanReadable = isLikelyHumanReadable;
9
+ exports.parseQueryString = parseQueryString;
10
+ exports.truncateAsString = truncateAsString;
11
+ var _stringify = require("../util/stringify");
12
+ var _constants = require("../../features/ajax/constants");
13
+ /**
14
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ /**
19
+ * Determines whether payload data should be captured based on the capture mode setting,
20
+ * HTTP status code, and GraphQL error status.
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
24
+ * @returns {boolean} True if payload should be captured
25
+ */
26
+ function canCapturePayload(captureMode, statusCode, hasGQLErrors) {
27
+ if (captureMode === _constants.CAPTURE_PAYLOAD_SETTINGS.ALL) return true;
28
+ if (!captureMode || captureMode === _constants.CAPTURE_PAYLOAD_SETTINGS.NONE) return false;
29
+
30
+ // Default "failures" mode
31
+ return statusCode === 0 || statusCode >= 400 || hasGQLErrors === true;
32
+ }
33
+
34
+ /**
35
+ * Parses a query string into an object of key-value pairs.
36
+ * @param {string} search a query string starting with "?" or without (ex. new URL(...).search)
37
+ * @returns {Object|undefined} Parsed query parameters as key-value pairs. Returns undefined if no valid parameters are found.
38
+ */
39
+ function parseQueryString(search) {
40
+ if (!search || search.length === 0) return;
41
+ const queryParams = {};
42
+ try {
43
+ const searchParams = new URLSearchParams(search);
44
+ searchParams.forEach(function (value, key) {
45
+ queryParams[key] = value;
46
+ });
47
+ } catch (e) {
48
+ // Fallback for environments without URLSearchParams
49
+ }
50
+ return queryParams;
51
+ }
52
+
53
+ /**
54
+ * Determines if the given content type is likely to be human-readable (text-based).
55
+ * @param {Object} headers - The headers object containing content-type
56
+ * @param {*} data - The data to check
57
+ * @returns {boolean} True if the content type is human-readable (text-based)
58
+ */
59
+ function isLikelyHumanReadable(headers, data) {
60
+ if (!headers) return typeof data === 'string';
61
+ var contentType = headers['content-type'];
62
+ if (!contentType) return typeof data === 'string';
63
+ // Normalize to lowercase and extract the mime type (ignore charset, etc.)
64
+ var mimeType = contentType.toLowerCase().split(';')[0].trim();
65
+
66
+ // Check for text/* types
67
+ if (mimeType.indexOf('text/') === 0) return true;
68
+
69
+ // Check for specific application/* types
70
+ var readableAppTypes = ['/json', '/xml', '/xhtml+xml', '/ld+json', '/yaml', '/x-www-form-urlencoded'];
71
+ for (var i = 0; i < readableAppTypes.length; i++) {
72
+ if (mimeType === 'application' + readableAppTypes[i]) return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Truncates a string to ensure its UTF-8 byte length does not exceed 4092 bytes. If truncation is necessary,
79
+ * the string is cut off at a character boundary to avoid breaking multi-byte characters and " ..." is appended to indicate truncation.
80
+ * If not a string, it is first converted to a string using JSON.stringify.
81
+ * @param {*} data The data to truncate.
82
+ * @returns {string}
83
+ */
84
+ function truncateAsString(data) {
85
+ if (!data) return data;
86
+ try {
87
+ if (typeof data !== 'string') data = (0, _stringify.stringify)(data);
88
+ let bytes = 0;
89
+ let i = 0;
90
+ let needsEllipsis = false;
91
+ while (i < data.length) {
92
+ const c = data.charCodeAt(i);
93
+ const charBytes = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0xD800 || c >= 0xE000 ? 3 : 4;
94
+ if (bytes + charBytes > 4092) {
95
+ needsEllipsis = true;
96
+ break;
97
+ }
98
+ bytes += charBytes;
99
+ i += charBytes === 4 ? 2 : 1;
100
+ }
101
+ return data.slice(0, i) + (needsEllipsis ? ' ...' : '');
102
+ } catch (e) {
103
+ return data;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Creates string adder functions for BEL serialization with obfuscation and optional truncation.
109
+ * This ensures a single string table is used while providing separate handling for regular vs payload attributes.
110
+ * @param {Function} getAddStringContext - Function that creates a new string table context
111
+ * @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
112
+ * @returns {{addString: Function, addStringWithTruncation: Function}} Object containing both string adder functions
113
+ */
114
+ function createStringAdders(getAddStringContext, obfuscator) {
115
+ const addStringRaw = getAddStringContext();
116
+ const processString = (str, shouldTruncate) => {
117
+ if (typeof str === 'undefined' || str === '') return addStringRaw(str);
118
+ if (typeof str !== 'string') str = (0, _stringify.stringify)(str);
119
+ const obfuscated = obfuscator?.obfuscateString(str) ?? str;
120
+ const processed = shouldTruncate ? truncateAsString(obfuscated) : obfuscated;
121
+ return addStringRaw(processed);
122
+ };
123
+ return {
124
+ addString: str => processString(str, false),
125
+ addStringWithTruncation: str => processString(str, true)
126
+ };
127
+ }
@@ -24,13 +24,14 @@ function numeric(n, noDefault) {
24
24
  }
25
25
  return n === undefined || n === 0 ? '' : Math.floor(n).toString(36);
26
26
  }
27
- function getAddStringContext(obfuscator) {
27
+ 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
31
  function addString(str) {
32
32
  if (typeof str === 'undefined' || str === '') return '';
33
33
  str = obfuscator?.obfuscateString(String(str)) ?? String(str);
34
+ str = truncator?.(str) ?? str;
34
35
  if (hasOwnProp.call(stringTable, str)) {
35
36
  return numeric(stringTable[str], true);
36
37
  } else {
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.dataSize = dataSize;
7
7
  var _stringify = require("./stringify");
8
8
  /**
9
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
9
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
10
10
  * SPDX-License-Identifier: Apache-2.0
11
11
  */
12
12
 
@@ -17,7 +17,7 @@ var _stringify = require("./stringify");
17
17
  * @returns {(number|undefined)} - The size of the data or undefined if size cannot be determined.
18
18
  */
19
19
  function dataSize(data) {
20
- if (typeof data === 'string' && data.length) return data.length;
20
+ if (typeof data === 'string') return data.length;
21
21
  if (typeof data !== 'object') return undefined;
22
22
  // eslint-disable-next-line
23
23
  if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer && data.byteLength) return data.byteLength;
@@ -23,7 +23,7 @@ var _utils = require("../v2/utils");
23
23
  */
24
24
 
25
25
  const wrapped = {};
26
- const XHR_PROPS = ['open', 'send']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
26
+ const XHR_PROPS = ['open', 'send', 'setRequestHeader']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
27
27
 
28
28
  /**
29
29
  * Wraps the native XMLHttpRequest (XHR) object to emit custom events to its readystatechange event and an assortment
@@ -3,10 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.hasGQLErrors = hasGQLErrors;
6
7
  exports.parseGQL = parseGQL;
7
8
  var _typeCheck = require("../../../common/util/type-check");
8
9
  /**
9
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
10
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
10
11
  * SPDX-License-Identifier: Apache-2.0
11
12
  */
12
13
 
@@ -95,4 +96,42 @@ function parseGQLQueryString(gqlQueryString) {
95
96
  }
96
97
  function validateGQLObject(obj) {
97
98
  return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string');
99
+ }
100
+
101
+ /**
102
+ * Checks if a response object has valid GraphQL errors.
103
+ * @param {object} response - A single GraphQL response object
104
+ * @returns {boolean} True if the response has valid errors
105
+ */
106
+ function hasValidGQLErrors(response) {
107
+ return Array.isArray(response?.errors) && response.errors.some(err => err && typeof err === 'object' && typeof err.message === 'string');
108
+ }
109
+
110
+ /**
111
+ * Checks if a response body contains GraphQL errors according to the GraphQL spec.
112
+ * A valid GraphQL error response contains an "errors" array with at least one error object.
113
+ * Supports both single and batched GraphQL responses.
114
+ * @param {string|object|array} [responseBody] The response body to check
115
+ * @returns {boolean} True if the response contains GraphQL errors
116
+ */
117
+ function hasGQLErrors(responseBody) {
118
+ if (!responseBody) return false;
119
+ try {
120
+ let parsed = responseBody;
121
+
122
+ // Parse string to object if needed
123
+ if (typeof responseBody === 'string') {
124
+ parsed = JSON.parse(responseBody);
125
+ }
126
+
127
+ // Handle batched GraphQL responses (array of response objects)
128
+ if (Array.isArray(parsed)) {
129
+ return parsed.some(hasValidGQLErrors);
130
+ }
131
+
132
+ // Handle single GraphQL response
133
+ return hasValidGQLErrors(parsed);
134
+ } catch (err) {
135
+ return false;
136
+ }
98
137
  }
@@ -11,9 +11,10 @@ var _denyList = require("../../../common/deny-list/deny-list");
11
11
  var _constants = require("../constants");
12
12
  var _features = require("../../../loaders/features/features");
13
13
  var _aggregateBase = require("../../utils/aggregate-base");
14
- var _gql = require("./gql");
15
14
  var _belSerializer = require("../../../common/serialize/bel-serializer");
16
15
  var _nreum = require("../../../common/window/nreum");
16
+ var _gql = require("./gql");
17
+ var _payloads = require("../../../common/payloads/payloads");
17
18
  var _obfuscate = require("../../../common/util/obfuscate");
18
19
  var _utils = require("../../../common/v2/utils");
19
20
  var _events = require("../../../common/constants/events");
@@ -96,17 +97,26 @@ class Aggregate extends _aggregateBase.AggregateBase {
96
97
  callbackDuration: metrics.cbTime,
97
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
98
99
  };
100
+ event.gql = params.gql = (0, _gql.parseGQL)({
101
+ body: ctx.requestBody,
102
+ query: ctx.parsedOrigin?.search
103
+ });
104
+ if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = (0, _gql.hasGQLErrors)(ctx.responseBody);
105
+ const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads;
106
+ const shouldCapturePayload = (0, _payloads.canCapturePayload)(capturePayloadSetting, params.status, event.gql?.operationHasErrors);
107
+ if (shouldCapturePayload) {
108
+ // 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;
114
+ }
99
115
  if (ctx.dt) {
100
116
  event.spanId = ctx.dt.spanId;
101
117
  event.traceId = ctx.dt.traceId;
102
118
  event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
103
119
  }
104
-
105
- // parsed from the AJAX body, looking for operationName param & parsing query for operationType
106
- event.gql = params.gql = (0, _gql.parseGQL)({
107
- body: ctx.body,
108
- query: ctx.parsedOrigin?.search
109
- });
110
120
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', (0, _stringify.stringify)(event.gql).length);
111
121
 
112
122
  /** make a copy of the event for the MFE target if it exists */
@@ -134,7 +144,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
134
144
  }
135
145
  serializer(eventBuffer) {
136
146
  if (!eventBuffer.length) return;
137
- const addString = (0, _belSerializer.getAddStringContext)(this.obfuscator);
147
+ const {
148
+ addString,
149
+ addStringWithTruncation
150
+ } = (0, _payloads.createStringAdders)(_belSerializer.getAddStringContext, this.obfuscator);
138
151
  let payload = 'bel.7;';
139
152
  let firstTimestamp = 0;
140
153
  for (let i = 0; i < eventBuffer.length; i++) {
@@ -159,15 +172,35 @@ class Aggregate extends _aggregateBase.AggregateBase {
159
172
  // 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.
160
173
  const jsAttributes = this.agentRef.info.jsAttributes;
161
174
 
162
- // add custom attributes
163
- // gql decorators are added as custom attributes to alleviate need for new BEL schema
164
- const attrParts = (0, _belSerializer.addCustomAttributes)({
175
+ // Regular attributes: obfuscate only
176
+ const regularAttrs = (0, _belSerializer.addCustomAttributes)({
165
177
  ...(jsAttributes || {}),
166
- ...(event.gql || {}),
178
+ [_constants.AJAX_ID]: event[_constants.AJAX_ID],
179
+ // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
167
180
  ...(event.targetAttributes || {}),
168
181
  // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
169
- [_constants.AJAX_ID]: event[_constants.AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
182
+ ...(event.gql || {})
170
183
  }, addString);
184
+
185
+ // Payload attributes: obfuscate then truncate
186
+ const payloadAttrs = (0, _belSerializer.addCustomAttributes)({
187
+ ...(event.requestBody ? {
188
+ requestBody: event.requestBody
189
+ } : {}),
190
+ ...(event.requestHeaders ? {
191
+ requestHeaders: event.requestHeaders
192
+ } : {}),
193
+ ...(event.requestQuery ? {
194
+ requestQuery: event.requestQuery
195
+ } : {}),
196
+ ...(event.responseBody ? {
197
+ responseBody: event.responseBody
198
+ } : {}),
199
+ ...(event.responseHeaders ? {
200
+ responseHeaders: event.responseHeaders
201
+ } : {})
202
+ }, addStringWithTruncation);
203
+ const attrParts = [...regularAttrs, ...payloadAttrs];
171
204
  fields.unshift((0, _belSerializer.numeric)(attrParts.length));
172
205
  insert += fields.join(',');
173
206
  if (attrParts && attrParts.length > 0) {
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.FEATURE_NAME = exports.AJAX_ID = void 0;
6
+ exports.FEATURE_NAME = exports.CAPTURE_PAYLOAD_SETTINGS = exports.AJAX_ID = void 0;
7
7
  var _features = require("../../loaders/features/features");
8
8
  /**
9
9
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
@@ -11,4 +11,9 @@ var _features = require("../../loaders/features/features");
11
11
  */
12
12
 
13
13
  const FEATURE_NAME = exports.FEATURE_NAME = _features.FEATURE_NAMES.ajax;
14
+ const CAPTURE_PAYLOAD_SETTINGS = exports.CAPTURE_PAYLOAD_SETTINGS = {
15
+ NONE: 'none',
16
+ FAILURES: 'failures',
17
+ ALL: 'all'
18
+ };
14
19
  const AJAX_ID = exports.AJAX_ID = 'ajaxRequest.id';
@@ -31,6 +31,7 @@ var handlersLen = handlers.length;
31
31
  var origRequest = (0, _nreum.gosNREUMOriginals)().o.REQ;
32
32
  var origXHR = (0, _nreum.gosNREUMOriginals)().o.XHR;
33
33
  const NR_CAT_HEADER = 'X-NewRelic-App-Data';
34
+ const INTERNAL_ERROR = 'internal-error';
34
35
  class Instrument extends _instrumentBase.InstrumentBase {
35
36
  static featureName = _constants.FEATURE_NAME;
36
37
  constructor(agentRef) {
@@ -77,10 +78,12 @@ class Instrument extends _instrumentBase.InstrumentBase {
77
78
  }
78
79
  exports.Instrument = Instrument;
79
80
  function subscribeToEvents(agentRef, ee, handler, dt) {
81
+ const shouldInterceptPayloads = [_constants.CAPTURE_PAYLOAD_SETTINGS.ALL, _constants.CAPTURE_PAYLOAD_SETTINGS.FAILURES].includes(agentRef.init.ajax?.capture_payloads);
80
82
  ee.on('new-xhr', onNewXhr);
81
83
  ee.on('open-xhr-start', onOpenXhrStart);
82
84
  ee.on('open-xhr-end', onOpenXhrEnd);
83
85
  ee.on('send-xhr-start', onSendXhrStart);
86
+ ee.on('setRequestHeader-xhr-start', onSetRequestHeader);
84
87
  ee.on('xhr-cb-time', onXhrCbTime);
85
88
  ee.on('xhr-load-added', onXhrLoadAdded);
86
89
  ee.on('xhr-load-removed', onXhrLoadRemoved);
@@ -152,6 +155,13 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
152
155
  }
153
156
  }
154
157
  }
158
+ function onSetRequestHeader(args, xhr) {
159
+ // args[0] = header name, args[1] = header value
160
+ if (shouldInterceptPayloads && args.length >= 2) {
161
+ this.requestHeaders ??= {};
162
+ this.requestHeaders[args[0].toLowerCase()] = args[1];
163
+ }
164
+ }
155
165
  function onSendXhrStart(args, xhr) {
156
166
  var metrics = this.metrics;
157
167
  var data = args[0];
@@ -161,7 +171,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
161
171
  if (size) metrics.txSize = size;
162
172
  }
163
173
  this.startTime = (0, _now.now)();
164
- this.body = data;
174
+ this.requestBody = data;
165
175
  this.listener = function (evt) {
166
176
  try {
167
177
  if (evt.type === 'abort' && !context.loadCaptureCalled) {
@@ -170,7 +180,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
170
180
  if (evt.type !== 'load' || context.called === context.totalCbs && (context.onloadCalled || typeof xhr.onload !== 'function') && typeof context.end === 'function') context.end(xhr);
171
181
  } catch (e) {
172
182
  try {
173
- ee.emit('internal-error', [e]);
183
+ ee.emit(INTERNAL_ERROR, [e]);
174
184
  } catch (err) {
175
185
  // do nothing
176
186
  }
@@ -298,8 +308,27 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
298
308
  addUrl(this, (0, _extractUrl.extractUrl)(target));
299
309
  const method = ('' + (target && target instanceof origRequest && target.method || opts.method || 'GET')).toUpperCase();
300
310
  this.params.method = method;
301
- this.body = opts.body;
302
- this.txSize = (0, _dataSize.dataSize)(opts.body) || 0;
311
+ this.txSize = (0, _dataSize.dataSize)(opts.body || target?.body) || 0;
312
+
313
+ // Capture request headers
314
+ try {
315
+ var headers = opts.headers || target?.headers;
316
+ if (shouldInterceptPayloads && headers) {
317
+ this.requestHeaders ??= {};
318
+ if (headers instanceof Headers) {
319
+ headers.forEach(function (value, key) {
320
+ this.requestHeaders[key.toLowerCase()] = value;
321
+ }.bind(this));
322
+ } else if (typeof headers === 'object') {
323
+ for (var key in headers) {
324
+ this.requestHeaders[key.toLowerCase()] = headers[key];
325
+ }
326
+ }
327
+ }
328
+ } catch (e) {
329
+ // Silently fail if we can't access headers
330
+ }
331
+ this.requestBody = opts.body || target?.body;
303
332
  }
304
333
 
305
334
  // we capture failed call as status 0, the actual error is ignored
@@ -310,19 +339,45 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
310
339
  if ((0, _denyList.hasUndefinedHostname)(this.params)) return; // don't bother with fetch to url with no hostname
311
340
 
312
341
  this.params.status = res ? res.status : 0;
342
+ const finishAndReport = () => {
343
+ // convert rxSize to a number - handle both string (from content-length header) and number (from fallback)
344
+ const num = +this.rxSize;
345
+ const responseSize = this.rxSize != null && !isNaN(num) ? num : undefined;
346
+ const metrics = {
347
+ txSize: this.txSize,
348
+ rxSize: responseSize,
349
+ duration: this.endTime - this.startTime
350
+ };
351
+ const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch'];
352
+ this.targets.forEach(target => reportToAgg(payload, this, target));
353
+ };
313
354
 
314
- // convert rxSize to a number
315
- let responseSize;
316
- if (typeof this.rxSize === 'string' && this.rxSize.length > 0) {
317
- responseSize = +this.rxSize;
355
+ /** Since accessing fetch bodies is an async process, these are
356
+ * reasonable conditions to check to not do needless extra work */
357
+ if (!res || !shouldInterceptPayloads) {
358
+ finishAndReport();
359
+ return;
318
360
  }
319
- const metrics = {
320
- txSize: this.txSize,
321
- rxSize: responseSize,
322
- duration: (0, _now.now)() - this.startTime
323
- };
324
- const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch'];
325
- this.targets.forEach(target => reportToAgg(payload, this, target));
361
+
362
+ // Clone the response to read the body without consuming the original
363
+ res.clone().text().then(text => {
364
+ this.responseBody = text;
365
+ // Use captured payload size as fallback if content-length header was missing or is 0 with a body
366
+ // Only apply fallback for non-network-error responses (status !== 0)
367
+ if ((!this.rxSize || this.rxSize === '0' || this.rxSize === 0) && text !== undefined && this.params.status !== 0) {
368
+ this.rxSize = (0, _dataSize.dataSize)(text);
369
+ }
370
+ if (res?.headers) {
371
+ this.responseHeaders = {};
372
+ res.headers.forEach(function (value, key) {
373
+ this.responseHeaders[key.toLowerCase()] = value;
374
+ }.bind(this));
375
+ }
376
+ }).catch(err => {
377
+ ee.emit(INTERNAL_ERROR, [err]);
378
+ }).finally(() => {
379
+ finishAndReport();
380
+ });
326
381
  }
327
382
 
328
383
  // Create report for XHR request that has finished
@@ -337,7 +392,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
337
392
  if (params.aborted) return;
338
393
  if ((0, _denyList.hasUndefinedHostname)(params)) return; // don't bother with XHR of url with no hostname
339
394
 
340
- metrics.duration = (0, _now.now)() - this.startTime;
395
+ metrics.duration = this.endTime - this.startTime;
341
396
  if (!this.loadCaptureCalled && xhr.readyState === 4) {
342
397
  captureXhrData(this, xhr);
343
398
  } else if (params.status == null) {
@@ -346,6 +401,25 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
346
401
 
347
402
  // Always send cbTime, even if no noticeable time was taken.
348
403
  metrics.cbTime = this.cbTime;
404
+ if (shouldInterceptPayloads) {
405
+ try {
406
+ this.responseBody = xhr.responseText;
407
+ } catch (e) {
408
+ this.responseBody = xhr.response;
409
+ }
410
+
411
+ // Use captured payload size as fallback if not already determined or is 0 with a body
412
+ // Only apply fallback for non-network-error responses (status !== 0)
413
+ if ((!metrics.rxSize || metrics.rxSize === 0) && this.responseBody !== undefined && params.status !== 0) {
414
+ const size = (0, _dataSize.dataSize)(this.responseBody);
415
+ if (size !== undefined) metrics.rxSize = size;
416
+ }
417
+ try {
418
+ this.responseHeaders = parseResponseHeaders(xhr.getAllResponseHeaders());
419
+ } catch (err) {
420
+ ee.emit(INTERNAL_ERROR, [err]);
421
+ }
422
+ }
349
423
  const payload = [params, metrics, this.startTime, this.endTime, 'xhr'];
350
424
  this.targets.forEach(target => reportToAgg(payload, this, target));
351
425
  }
@@ -355,7 +429,8 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
355
429
  function captureXhrData(ctx, xhr) {
356
430
  ctx.params.status = xhr.status;
357
431
  var size = (0, _responseSize.responseSizeFromXhr)(xhr, ctx.lastSize);
358
- if (size) ctx.metrics.rxSize = size;
432
+ // Don't set rxSize for network errors (status 0)
433
+ if (size !== undefined && xhr.status !== 0) ctx.metrics.rxSize = size;
359
434
  if (ctx.sameOrigin && xhr.getAllResponseHeaders().indexOf(NR_CAT_HEADER) >= 0) {
360
435
  var header = xhr.getResponseHeader(NR_CAT_HEADER);
361
436
  if (header) {
@@ -377,4 +452,17 @@ function addUrl(ctx, url) {
377
452
  ctx.parsedOrigin = parsed;
378
453
  ctx.sameOrigin = parsed.sameOrigin;
379
454
  }
455
+ function parseResponseHeaders(headerStr) {
456
+ const headers = {};
457
+ if (!headerStr) return headers;
458
+ headerStr.split('\r\n').forEach(function (line) {
459
+ const separatorIndex = line.indexOf(': ');
460
+ if (separatorIndex > 0) {
461
+ const name = line.substring(0, separatorIndex);
462
+ const value = line.substring(separatorIndex + 2);
463
+ headers[name.toLowerCase()] = value;
464
+ }
465
+ });
466
+ return headers;
467
+ }
380
468
  const Ajax = exports.Ajax = Instrument;
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.formatStackTrace = formatStackTrace;
7
7
  exports.truncateSize = truncateSize;
8
8
  /**
9
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
9
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
10
10
  * SPDX-License-Identifier: Apache-2.0
11
11
  */
12
12
 
@@ -16,7 +16,7 @@ function formatStackTrace(stackLines) {
16
16
  return truncateStackLines(stackLines).replace(stripNewlinesRegex, '');
17
17
  }
18
18
 
19
- // takes array of stack lines and returns string with top 50 and buttom 50 lines
19
+ // takes array of stack lines and returns string with top 50 and bottom 50 lines
20
20
  function truncateStackLines(stackLines) {
21
21
  var stackString;
22
22
  if (stackLines.length > 100) {
@@ -267,7 +267,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
267
267
  params.stack_trace = (0, _formatStackTrace.truncateSize)(stackInfo.stackString);
268
268
  this.observedAt[bucketHash] = Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(time));
269
269
  } else {
270
- params.browser_stack_hash = (0, _stringHashCode.stringHashCode)(stackInfo.stackString);
270
+ params.browser_stack_hash = (0, _stringHashCode.stringHashCode)((0, _formatStackTrace.truncateSize)(stackInfo.stackString));
271
271
  }
272
272
  params.releaseIds = (0, _stringify.stringify)(this.agentRef.runtime.releaseIds);
273
273