@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.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/common/config/init-types.js +1 -0
- package/dist/cjs/common/config/init.js +6 -4
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/payloads/payloads.js +127 -0
- package/dist/cjs/common/serialize/bel-serializer.js +2 -1
- package/dist/cjs/common/util/data-size.js +2 -2
- package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
- package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
- package/dist/cjs/features/ajax/aggregate/index.js +46 -13
- package/dist/cjs/features/ajax/constants.js +6 -1
- package/dist/cjs/features/ajax/instrument/index.js +105 -17
- package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
- package/dist/cjs/features/jserrors/aggregate/index.js +1 -1
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +35 -4
- package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/dist/esm/common/config/init-types.js +1 -0
- package/dist/esm/common/config/init.js +3 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/payloads/payloads.js +118 -0
- package/dist/esm/common/serialize/bel-serializer.js +2 -1
- package/dist/esm/common/util/data-size.js +2 -2
- package/dist/esm/common/wrap/wrap-xhr.js +1 -1
- package/dist/esm/features/ajax/aggregate/gql.js +39 -1
- package/dist/esm/features/ajax/aggregate/index.js +47 -14
- package/dist/esm/features/ajax/constants.js +5 -0
- package/dist/esm/features/ajax/instrument/index.js +106 -18
- package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
- package/dist/esm/features/jserrors/aggregate/index.js +1 -1
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +35 -4
- package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/init-types.d.ts +4 -0
- package/dist/types/common/config/init-types.d.ts.map +1 -1
- package/dist/types/common/config/init.d.ts.map +1 -1
- package/dist/types/common/payloads/payloads.d.ts +42 -0
- package/dist/types/common/payloads/payloads.d.ts.map +1 -0
- package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
- package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
- package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/constants.d.ts +5 -0
- package/dist/types/features/ajax/constants.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +5 -0
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/init-types.js +1 -0
- package/src/common/config/init.js +2 -1
- package/src/common/payloads/payloads.js +135 -0
- package/src/common/serialize/bel-serializer.js +2 -1
- package/src/common/util/data-size.js +2 -2
- package/src/common/wrap/wrap-xhr.js +1 -1
- package/src/features/ajax/aggregate/gql.js +42 -1
- package/src/features/ajax/aggregate/index.js +38 -13
- package/src/features/ajax/constants.js +5 -1
- package/src/features/ajax/instrument/index.js +114 -18
- package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
- package/src/features/jserrors/aggregate/index.js +1 -1
- package/src/features/soft_navigations/aggregate/ajax-node.js +25 -4
- 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
|
|
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:
|
|
145
|
-
inactiveMs:
|
|
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.
|
|
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.
|
|
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-
|
|
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'
|
|
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-
|
|
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
|
|
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
|
-
//
|
|
163
|
-
|
|
164
|
-
const attrParts = (0, _belSerializer.addCustomAttributes)({
|
|
175
|
+
// Regular attributes: obfuscate only
|
|
176
|
+
const regularAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
165
177
|
...(jsAttributes || {}),
|
|
166
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
302
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|