@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
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.AjaxNode = void 0;
|
|
7
7
|
var _belSerializer = require("../../../common/serialize/bel-serializer");
|
|
8
|
+
var _payloads = require("../../../common/payloads/payloads");
|
|
8
9
|
var _constants = require("../../ajax/constants");
|
|
9
10
|
var _constants2 = require("../constants");
|
|
10
11
|
var _belNode = require("./bel-node");
|
|
@@ -31,6 +32,12 @@ class AjaxNode extends _belNode.BelNode {
|
|
|
31
32
|
this.targetAttributes = ajaxEvent.targetAttributes;
|
|
32
33
|
this[_constants.AJAX_ID] = ajaxEvent[_constants.AJAX_ID]; // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
33
34
|
|
|
35
|
+
// optional payload metadata attributes
|
|
36
|
+
this.requestBody = ajaxEvent.requestBody;
|
|
37
|
+
this.requestHeaders = ajaxEvent.requestHeaders;
|
|
38
|
+
this.requestQuery = ajaxEvent.requestQuery;
|
|
39
|
+
this.responseBody = ajaxEvent.responseBody;
|
|
40
|
+
this.responseHeaders = ajaxEvent.responseHeaders;
|
|
34
41
|
this.start = ajaxEvent.startTime;
|
|
35
42
|
this.end = ajaxEvent.endTime;
|
|
36
43
|
if (ajaxContext?.latestLongtaskEnd) {
|
|
@@ -39,7 +46,10 @@ class AjaxNode extends _belNode.BelNode {
|
|
|
39
46
|
} else this.callbackEnd = this.end; // if no long task was observed, callbackEnd is the same as end
|
|
40
47
|
}
|
|
41
48
|
serialize(parentStartTimestamp, agentRef, ajaxObfuscator) {
|
|
42
|
-
const
|
|
49
|
+
const {
|
|
50
|
+
addString,
|
|
51
|
+
addStringWithTruncation
|
|
52
|
+
} = (0, _payloads.createStringAdders)(_belSerializer.getAddStringContext, ajaxObfuscator);
|
|
43
53
|
const nodeList = [];
|
|
44
54
|
|
|
45
55
|
// IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
|
|
@@ -54,11 +64,32 @@ class AjaxNode extends _belNode.BelNode {
|
|
|
54
64
|
(0, _belSerializer.numeric)(this.callbackDuration),
|
|
55
65
|
// not relative
|
|
56
66
|
addString(this.method), (0, _belSerializer.numeric)(this.status), addString(this.domain), addString(this.path), (0, _belSerializer.numeric)(this.txSize), (0, _belSerializer.numeric)(this.rxSize), this.requestedWith, addString(this.nodeId), (0, _belSerializer.nullable)(this.spanId, addString, true) + (0, _belSerializer.nullable)(this.traceId, addString, true) + (0, _belSerializer.nullable)(this.spanTimestamp, _belSerializer.numeric)];
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
// Regular attributes: obfuscate only
|
|
68
|
+
const regularAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
69
|
+
[_constants.AJAX_ID]: this[_constants.AJAX_ID],
|
|
59
70
|
...(this.targetAttributes || {}),
|
|
60
|
-
|
|
71
|
+
...(this.gql || {})
|
|
61
72
|
}, addString);
|
|
73
|
+
|
|
74
|
+
// Payload attributes: obfuscate then truncate
|
|
75
|
+
const payloadAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
76
|
+
...(this.requestBody ? {
|
|
77
|
+
requestBody: this.requestBody
|
|
78
|
+
} : {}),
|
|
79
|
+
...(this.requestHeaders ? {
|
|
80
|
+
requestHeaders: this.requestHeaders
|
|
81
|
+
} : {}),
|
|
82
|
+
...(this.requestQuery ? {
|
|
83
|
+
requestQuery: this.requestQuery
|
|
84
|
+
} : {}),
|
|
85
|
+
...(this.responseBody ? {
|
|
86
|
+
responseBody: this.responseBody
|
|
87
|
+
} : {}),
|
|
88
|
+
...(this.responseHeaders ? {
|
|
89
|
+
responseHeaders: this.responseHeaders
|
|
90
|
+
} : {})
|
|
91
|
+
}, addStringWithTruncation);
|
|
92
|
+
let allAttachedNodes = [...regularAttrs, ...payloadAttrs];
|
|
62
93
|
this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
|
|
63
94
|
|
|
64
95
|
fields[1] = (0, _belSerializer.numeric)(allAttachedNodes.length);
|
|
@@ -11,7 +11,7 @@ var _firstPaint = require("../../../common/vitals/first-paint");
|
|
|
11
11
|
var _firstContentfulPaint = require("../../../common/vitals/first-contentful-paint");
|
|
12
12
|
var _constants = require("../constants");
|
|
13
13
|
/**
|
|
14
|
-
* Copyright 2020-
|
|
14
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @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.
|
|
12
12
|
* @property {boolean} [ajax.enabled] - Turn on/off the ajax feature (on by default).
|
|
13
13
|
* @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.
|
|
14
|
+
* @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.
|
|
14
15
|
* @property {Object} [api]
|
|
15
16
|
* @property {Object} [api.register]
|
|
16
17
|
* @property {boolean} [api.register.enabled] - If true, the agent will allow registered children to be sent to the server.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { FEATURE_FLAGS } from '../../features/generic_events/constants';
|
|
6
|
+
import { CAPTURE_PAYLOAD_SETTINGS } from '../../features/ajax/constants';
|
|
6
7
|
import { isValidSelector } from '../dom/query-selector';
|
|
7
8
|
import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS } from '../session/constants';
|
|
8
9
|
import { warn } from '../util/console';
|
|
@@ -51,7 +52,8 @@ const InitModelFn = () => {
|
|
|
51
52
|
deny_list: undefined,
|
|
52
53
|
block_internal: true,
|
|
53
54
|
enabled: true,
|
|
54
|
-
autoStart: true
|
|
55
|
+
autoStart: true,
|
|
56
|
+
capture_payloads: CAPTURE_PAYLOAD_SETTINGS.NONE
|
|
55
57
|
},
|
|
56
58
|
api: {
|
|
57
59
|
register: {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { stringify } from '../util/stringify';
|
|
7
|
+
import { CAPTURE_PAYLOAD_SETTINGS } from '../../features/ajax/constants';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether payload data should be captured based on the capture mode setting,
|
|
11
|
+
* HTTP status code, and GraphQL error status.
|
|
12
|
+
* @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
|
|
13
|
+
* @param {number} statusCode - The HTTP status code
|
|
14
|
+
* @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
|
|
15
|
+
* @returns {boolean} True if payload should be captured
|
|
16
|
+
*/
|
|
17
|
+
export function canCapturePayload(captureMode, statusCode, hasGQLErrors) {
|
|
18
|
+
if (captureMode === CAPTURE_PAYLOAD_SETTINGS.ALL) return true;
|
|
19
|
+
if (!captureMode || captureMode === CAPTURE_PAYLOAD_SETTINGS.NONE) return false;
|
|
20
|
+
|
|
21
|
+
// Default "failures" mode
|
|
22
|
+
return statusCode === 0 || statusCode >= 400 || hasGQLErrors === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses a query string into an object of key-value pairs.
|
|
27
|
+
* @param {string} search a query string starting with "?" or without (ex. new URL(...).search)
|
|
28
|
+
* @returns {Object|undefined} Parsed query parameters as key-value pairs. Returns undefined if no valid parameters are found.
|
|
29
|
+
*/
|
|
30
|
+
export function parseQueryString(search) {
|
|
31
|
+
if (!search || search.length === 0) return;
|
|
32
|
+
const queryParams = {};
|
|
33
|
+
try {
|
|
34
|
+
const searchParams = new URLSearchParams(search);
|
|
35
|
+
searchParams.forEach(function (value, key) {
|
|
36
|
+
queryParams[key] = value;
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Fallback for environments without URLSearchParams
|
|
40
|
+
}
|
|
41
|
+
return queryParams;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Determines if the given content type is likely to be human-readable (text-based).
|
|
46
|
+
* @param {Object} headers - The headers object containing content-type
|
|
47
|
+
* @param {*} data - The data to check
|
|
48
|
+
* @returns {boolean} True if the content type is human-readable (text-based)
|
|
49
|
+
*/
|
|
50
|
+
export function isLikelyHumanReadable(headers, data) {
|
|
51
|
+
if (!headers) return typeof data === 'string';
|
|
52
|
+
var contentType = headers['content-type'];
|
|
53
|
+
if (!contentType) return typeof data === 'string';
|
|
54
|
+
// Normalize to lowercase and extract the mime type (ignore charset, etc.)
|
|
55
|
+
var mimeType = contentType.toLowerCase().split(';')[0].trim();
|
|
56
|
+
|
|
57
|
+
// Check for text/* types
|
|
58
|
+
if (mimeType.indexOf('text/') === 0) return true;
|
|
59
|
+
|
|
60
|
+
// Check for specific application/* types
|
|
61
|
+
var readableAppTypes = ['/json', '/xml', '/xhtml+xml', '/ld+json', '/yaml', '/x-www-form-urlencoded'];
|
|
62
|
+
for (var i = 0; i < readableAppTypes.length; i++) {
|
|
63
|
+
if (mimeType === 'application' + readableAppTypes[i]) return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Truncates a string to ensure its UTF-8 byte length does not exceed 4092 bytes. If truncation is necessary,
|
|
70
|
+
* the string is cut off at a character boundary to avoid breaking multi-byte characters and " ..." is appended to indicate truncation.
|
|
71
|
+
* If not a string, it is first converted to a string using JSON.stringify.
|
|
72
|
+
* @param {*} data The data to truncate.
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function truncateAsString(data) {
|
|
76
|
+
if (!data) return data;
|
|
77
|
+
try {
|
|
78
|
+
if (typeof data !== 'string') data = stringify(data);
|
|
79
|
+
let bytes = 0;
|
|
80
|
+
let i = 0;
|
|
81
|
+
let needsEllipsis = false;
|
|
82
|
+
while (i < data.length) {
|
|
83
|
+
const c = data.charCodeAt(i);
|
|
84
|
+
const charBytes = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0xD800 || c >= 0xE000 ? 3 : 4;
|
|
85
|
+
if (bytes + charBytes > 4092) {
|
|
86
|
+
needsEllipsis = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
bytes += charBytes;
|
|
90
|
+
i += charBytes === 4 ? 2 : 1;
|
|
91
|
+
}
|
|
92
|
+
return data.slice(0, i) + (needsEllipsis ? ' ...' : '');
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
100
|
+
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
101
|
+
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
102
|
+
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
103
|
+
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing both string adder functions
|
|
104
|
+
*/
|
|
105
|
+
export function createStringAdders(getAddStringContext, obfuscator) {
|
|
106
|
+
const addStringRaw = getAddStringContext();
|
|
107
|
+
const processString = (str, shouldTruncate) => {
|
|
108
|
+
if (typeof str === 'undefined' || str === '') return addStringRaw(str);
|
|
109
|
+
if (typeof str !== 'string') str = stringify(str);
|
|
110
|
+
const obfuscated = obfuscator?.obfuscateString(str) ?? str;
|
|
111
|
+
const processed = shouldTruncate ? truncateAsString(obfuscated) : obfuscated;
|
|
112
|
+
return addStringRaw(processed);
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
addString: str => processString(str, false),
|
|
116
|
+
addStringWithTruncation: str => processString(str, true)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -15,13 +15,14 @@ export function numeric(n, noDefault) {
|
|
|
15
15
|
}
|
|
16
16
|
return n === undefined || n === 0 ? '' : Math.floor(n).toString(36);
|
|
17
17
|
}
|
|
18
|
-
export function getAddStringContext(obfuscator) {
|
|
18
|
+
export function getAddStringContext(obfuscator, truncator) {
|
|
19
19
|
let stringTableIdx = 0;
|
|
20
20
|
const stringTable = Object.prototype.hasOwnProperty.call(Object, 'create') ? Object.create(null) : {};
|
|
21
21
|
return addString;
|
|
22
22
|
function addString(str) {
|
|
23
23
|
if (typeof str === 'undefined' || str === '') return '';
|
|
24
24
|
str = obfuscator?.obfuscateString(String(str)) ?? String(str);
|
|
25
|
+
str = truncator?.(str) ?? str;
|
|
25
26
|
if (hasOwnProp.call(stringTable, str)) {
|
|
26
27
|
return numeric(stringTable[str], true);
|
|
27
28
|
} else {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ import { stringify } from './stringify';
|
|
|
12
12
|
* @returns {(number|undefined)} - The size of the data or undefined if size cannot be determined.
|
|
13
13
|
*/
|
|
14
14
|
export function dataSize(data) {
|
|
15
|
-
if (typeof data === 'string'
|
|
15
|
+
if (typeof data === 'string') return data.length;
|
|
16
16
|
if (typeof data !== 'object') return undefined;
|
|
17
17
|
// eslint-disable-next-line
|
|
18
18
|
if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer && data.byteLength) return data.byteLength;
|
|
@@ -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-
|
|
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,12 +6,13 @@ 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 {
|
|
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';
|
|
15
16
|
import { Obfuscator } from '../../../common/util/obfuscate';
|
|
16
17
|
import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils';
|
|
17
18
|
import { EVENT_TYPES } from '../../../common/constants/events';
|
|
@@ -89,17 +90,26 @@ export class Aggregate extends AggregateBase {
|
|
|
89
90
|
callbackDuration: metrics.cbTime,
|
|
90
91
|
[AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
91
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
|
+
}
|
|
92
108
|
if (ctx.dt) {
|
|
93
109
|
event.spanId = ctx.dt.spanId;
|
|
94
110
|
event.traceId = ctx.dt.traceId;
|
|
95
111
|
event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
|
|
96
112
|
}
|
|
97
|
-
|
|
98
|
-
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
99
|
-
event.gql = params.gql = parseGQL({
|
|
100
|
-
body: ctx.body,
|
|
101
|
-
query: ctx.parsedOrigin?.search
|
|
102
|
-
});
|
|
103
113
|
if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length);
|
|
104
114
|
|
|
105
115
|
/** make a copy of the event for the MFE target if it exists */
|
|
@@ -127,7 +137,10 @@ export class Aggregate extends AggregateBase {
|
|
|
127
137
|
}
|
|
128
138
|
serializer(eventBuffer) {
|
|
129
139
|
if (!eventBuffer.length) return;
|
|
130
|
-
const
|
|
140
|
+
const {
|
|
141
|
+
addString,
|
|
142
|
+
addStringWithTruncation
|
|
143
|
+
} = createStringAdders(getAddStringContext, this.obfuscator);
|
|
131
144
|
let payload = 'bel.7;';
|
|
132
145
|
let firstTimestamp = 0;
|
|
133
146
|
for (let i = 0; i < eventBuffer.length; i++) {
|
|
@@ -152,15 +165,35 @@ export class Aggregate extends AggregateBase {
|
|
|
152
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.
|
|
153
166
|
const jsAttributes = this.agentRef.info.jsAttributes;
|
|
154
167
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
const attrParts = addCustomAttributes({
|
|
168
|
+
// Regular attributes: obfuscate only
|
|
169
|
+
const regularAttrs = addCustomAttributes({
|
|
158
170
|
...(jsAttributes || {}),
|
|
159
|
-
|
|
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
|
|
160
173
|
...(event.targetAttributes || {}),
|
|
161
174
|
// used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
162
|
-
|
|
175
|
+
...(event.gql || {})
|
|
163
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];
|
|
164
197
|
fields.unshift(numeric(attrParts.length));
|
|
165
198
|
insert += fields.join(',');
|
|
166
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';
|
|
@@ -14,7 +14,7 @@ import { parseUrl } from '../../../common/url/parse-url';
|
|
|
14
14
|
import { DT } from './distributed-tracing';
|
|
15
15
|
import { responseSizeFromXhr } from './response-size';
|
|
16
16
|
import { InstrumentBase } from '../../utils/instrument-base';
|
|
17
|
-
import { FEATURE_NAME } from '../constants';
|
|
17
|
+
import { CAPTURE_PAYLOAD_SETTINGS, FEATURE_NAME } from '../constants';
|
|
18
18
|
import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
19
19
|
import { SUPPORTABILITY_METRIC } from '../../metrics/constants';
|
|
20
20
|
import { now } from '../../../common/timing/now';
|
|
@@ -25,6 +25,7 @@ var handlersLen = handlers.length;
|
|
|
25
25
|
var origRequest = gosNREUMOriginals().o.REQ;
|
|
26
26
|
var origXHR = gosNREUMOriginals().o.XHR;
|
|
27
27
|
const NR_CAT_HEADER = 'X-NewRelic-App-Data';
|
|
28
|
+
const INTERNAL_ERROR = 'internal-error';
|
|
28
29
|
export class Instrument extends InstrumentBase {
|
|
29
30
|
static featureName = FEATURE_NAME;
|
|
30
31
|
constructor(agentRef) {
|
|
@@ -70,10 +71,12 @@ export class Instrument extends InstrumentBase {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
74
|
+
const shouldInterceptPayloads = [CAPTURE_PAYLOAD_SETTINGS.ALL, CAPTURE_PAYLOAD_SETTINGS.FAILURES].includes(agentRef.init.ajax?.capture_payloads);
|
|
73
75
|
ee.on('new-xhr', onNewXhr);
|
|
74
76
|
ee.on('open-xhr-start', onOpenXhrStart);
|
|
75
77
|
ee.on('open-xhr-end', onOpenXhrEnd);
|
|
76
78
|
ee.on('send-xhr-start', onSendXhrStart);
|
|
79
|
+
ee.on('setRequestHeader-xhr-start', onSetRequestHeader);
|
|
77
80
|
ee.on('xhr-cb-time', onXhrCbTime);
|
|
78
81
|
ee.on('xhr-load-added', onXhrLoadAdded);
|
|
79
82
|
ee.on('xhr-load-removed', onXhrLoadRemoved);
|
|
@@ -145,6 +148,13 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
}
|
|
151
|
+
function onSetRequestHeader(args, xhr) {
|
|
152
|
+
// args[0] = header name, args[1] = header value
|
|
153
|
+
if (shouldInterceptPayloads && args.length >= 2) {
|
|
154
|
+
this.requestHeaders ??= {};
|
|
155
|
+
this.requestHeaders[args[0].toLowerCase()] = args[1];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
148
158
|
function onSendXhrStart(args, xhr) {
|
|
149
159
|
var metrics = this.metrics;
|
|
150
160
|
var data = args[0];
|
|
@@ -154,7 +164,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
154
164
|
if (size) metrics.txSize = size;
|
|
155
165
|
}
|
|
156
166
|
this.startTime = now();
|
|
157
|
-
this.
|
|
167
|
+
this.requestBody = data;
|
|
158
168
|
this.listener = function (evt) {
|
|
159
169
|
try {
|
|
160
170
|
if (evt.type === 'abort' && !context.loadCaptureCalled) {
|
|
@@ -163,7 +173,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
163
173
|
if (evt.type !== 'load' || context.called === context.totalCbs && (context.onloadCalled || typeof xhr.onload !== 'function') && typeof context.end === 'function') context.end(xhr);
|
|
164
174
|
} catch (e) {
|
|
165
175
|
try {
|
|
166
|
-
ee.emit(
|
|
176
|
+
ee.emit(INTERNAL_ERROR, [e]);
|
|
167
177
|
} catch (err) {
|
|
168
178
|
// do nothing
|
|
169
179
|
}
|
|
@@ -291,8 +301,27 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
291
301
|
addUrl(this, extractUrl(target));
|
|
292
302
|
const method = ('' + (target && target instanceof origRequest && target.method || opts.method || 'GET')).toUpperCase();
|
|
293
303
|
this.params.method = method;
|
|
294
|
-
this.
|
|
295
|
-
|
|
304
|
+
this.txSize = dataSize(opts.body || target?.body) || 0;
|
|
305
|
+
|
|
306
|
+
// Capture request headers
|
|
307
|
+
try {
|
|
308
|
+
var headers = opts.headers || target?.headers;
|
|
309
|
+
if (shouldInterceptPayloads && headers) {
|
|
310
|
+
this.requestHeaders ??= {};
|
|
311
|
+
if (headers instanceof Headers) {
|
|
312
|
+
headers.forEach(function (value, key) {
|
|
313
|
+
this.requestHeaders[key.toLowerCase()] = value;
|
|
314
|
+
}.bind(this));
|
|
315
|
+
} else if (typeof headers === 'object') {
|
|
316
|
+
for (var key in headers) {
|
|
317
|
+
this.requestHeaders[key.toLowerCase()] = headers[key];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
// Silently fail if we can't access headers
|
|
323
|
+
}
|
|
324
|
+
this.requestBody = opts.body || target?.body;
|
|
296
325
|
}
|
|
297
326
|
|
|
298
327
|
// we capture failed call as status 0, the actual error is ignored
|
|
@@ -303,19 +332,45 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
303
332
|
if (hasUndefinedHostname(this.params)) return; // don't bother with fetch to url with no hostname
|
|
304
333
|
|
|
305
334
|
this.params.status = res ? res.status : 0;
|
|
335
|
+
const finishAndReport = () => {
|
|
336
|
+
// convert rxSize to a number - handle both string (from content-length header) and number (from fallback)
|
|
337
|
+
const num = +this.rxSize;
|
|
338
|
+
const responseSize = this.rxSize != null && !isNaN(num) ? num : undefined;
|
|
339
|
+
const metrics = {
|
|
340
|
+
txSize: this.txSize,
|
|
341
|
+
rxSize: responseSize,
|
|
342
|
+
duration: this.endTime - this.startTime
|
|
343
|
+
};
|
|
344
|
+
const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch'];
|
|
345
|
+
this.targets.forEach(target => reportToAgg(payload, this, target));
|
|
346
|
+
};
|
|
306
347
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
348
|
+
/** Since accessing fetch bodies is an async process, these are
|
|
349
|
+
* reasonable conditions to check to not do needless extra work */
|
|
350
|
+
if (!res || !shouldInterceptPayloads) {
|
|
351
|
+
finishAndReport();
|
|
352
|
+
return;
|
|
311
353
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
354
|
+
|
|
355
|
+
// Clone the response to read the body without consuming the original
|
|
356
|
+
res.clone().text().then(text => {
|
|
357
|
+
this.responseBody = text;
|
|
358
|
+
// Use captured payload size as fallback if content-length header was missing or is 0 with a body
|
|
359
|
+
// Only apply fallback for non-network-error responses (status !== 0)
|
|
360
|
+
if ((!this.rxSize || this.rxSize === '0' || this.rxSize === 0) && text !== undefined && this.params.status !== 0) {
|
|
361
|
+
this.rxSize = dataSize(text);
|
|
362
|
+
}
|
|
363
|
+
if (res?.headers) {
|
|
364
|
+
this.responseHeaders = {};
|
|
365
|
+
res.headers.forEach(function (value, key) {
|
|
366
|
+
this.responseHeaders[key.toLowerCase()] = value;
|
|
367
|
+
}.bind(this));
|
|
368
|
+
}
|
|
369
|
+
}).catch(err => {
|
|
370
|
+
ee.emit(INTERNAL_ERROR, [err]);
|
|
371
|
+
}).finally(() => {
|
|
372
|
+
finishAndReport();
|
|
373
|
+
});
|
|
319
374
|
}
|
|
320
375
|
|
|
321
376
|
// Create report for XHR request that has finished
|
|
@@ -330,7 +385,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
330
385
|
if (params.aborted) return;
|
|
331
386
|
if (hasUndefinedHostname(params)) return; // don't bother with XHR of url with no hostname
|
|
332
387
|
|
|
333
|
-
metrics.duration =
|
|
388
|
+
metrics.duration = this.endTime - this.startTime;
|
|
334
389
|
if (!this.loadCaptureCalled && xhr.readyState === 4) {
|
|
335
390
|
captureXhrData(this, xhr);
|
|
336
391
|
} else if (params.status == null) {
|
|
@@ -339,6 +394,25 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
339
394
|
|
|
340
395
|
// Always send cbTime, even if no noticeable time was taken.
|
|
341
396
|
metrics.cbTime = this.cbTime;
|
|
397
|
+
if (shouldInterceptPayloads) {
|
|
398
|
+
try {
|
|
399
|
+
this.responseBody = xhr.responseText;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
this.responseBody = xhr.response;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Use captured payload size as fallback if not already determined or is 0 with a body
|
|
405
|
+
// Only apply fallback for non-network-error responses (status !== 0)
|
|
406
|
+
if ((!metrics.rxSize || metrics.rxSize === 0) && this.responseBody !== undefined && params.status !== 0) {
|
|
407
|
+
const size = dataSize(this.responseBody);
|
|
408
|
+
if (size !== undefined) metrics.rxSize = size;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
this.responseHeaders = parseResponseHeaders(xhr.getAllResponseHeaders());
|
|
412
|
+
} catch (err) {
|
|
413
|
+
ee.emit(INTERNAL_ERROR, [err]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
342
416
|
const payload = [params, metrics, this.startTime, this.endTime, 'xhr'];
|
|
343
417
|
this.targets.forEach(target => reportToAgg(payload, this, target));
|
|
344
418
|
}
|
|
@@ -348,7 +422,8 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
348
422
|
function captureXhrData(ctx, xhr) {
|
|
349
423
|
ctx.params.status = xhr.status;
|
|
350
424
|
var size = responseSizeFromXhr(xhr, ctx.lastSize);
|
|
351
|
-
|
|
425
|
+
// Don't set rxSize for network errors (status 0)
|
|
426
|
+
if (size !== undefined && xhr.status !== 0) ctx.metrics.rxSize = size;
|
|
352
427
|
if (ctx.sameOrigin && xhr.getAllResponseHeaders().indexOf(NR_CAT_HEADER) >= 0) {
|
|
353
428
|
var header = xhr.getResponseHeader(NR_CAT_HEADER);
|
|
354
429
|
if (header) {
|
|
@@ -370,4 +445,17 @@ function addUrl(ctx, url) {
|
|
|
370
445
|
ctx.parsedOrigin = parsed;
|
|
371
446
|
ctx.sameOrigin = parsed.sameOrigin;
|
|
372
447
|
}
|
|
448
|
+
function parseResponseHeaders(headerStr) {
|
|
449
|
+
const headers = {};
|
|
450
|
+
if (!headerStr) return headers;
|
|
451
|
+
headerStr.split('\r\n').forEach(function (line) {
|
|
452
|
+
const separatorIndex = line.indexOf(': ');
|
|
453
|
+
if (separatorIndex > 0) {
|
|
454
|
+
const name = line.substring(0, separatorIndex);
|
|
455
|
+
const value = line.substring(separatorIndex + 2);
|
|
456
|
+
headers[name.toLowerCase()] = value;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
return headers;
|
|
460
|
+
}
|
|
373
461
|
export const Ajax = Instrument;
|