@newrelic/browser-agent 1.316.0 → 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 +2 -2
- package/dist/cjs/common/constants/env.npm.js +2 -2
- package/dist/cjs/common/constants/events.js +26 -0
- package/dist/cjs/common/harvest/harvester.js +12 -4
- package/dist/cjs/common/payloads/payloads.js +127 -0
- package/dist/cjs/common/serialize/bel-serializer.js +4 -3
- package/dist/cjs/common/util/data-size.js +2 -2
- package/dist/cjs/common/util/obfuscate.js +147 -3
- package/dist/cjs/common/wrap/wrap-websocket.js +2 -1
- 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 +51 -13
- package/dist/cjs/features/ajax/constants.js +6 -1
- package/dist/cjs/features/ajax/instrument/index.js +105 -17
- package/dist/cjs/features/generic_events/aggregate/index.js +15 -10
- package/dist/cjs/features/generic_events/constants.js +3 -2
- package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
- package/dist/cjs/features/jserrors/aggregate/index.js +12 -3
- package/dist/cjs/features/logging/aggregate/index.js +7 -3
- package/dist/cjs/features/metrics/aggregate/index.js +5 -1
- package/dist/cjs/features/page_view_event/aggregate/index.js +7 -3
- package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +7 -1
- package/dist/cjs/features/session_trace/aggregate/index.js +6 -2
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +36 -5
- package/dist/cjs/features/soft_navigations/aggregate/index.js +7 -1
- package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +6 -4
- package/dist/cjs/features/utils/aggregate-base.js +2 -3
- 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 +2 -2
- package/dist/esm/common/constants/env.npm.js +2 -2
- package/dist/esm/common/constants/events.js +20 -0
- package/dist/esm/common/harvest/harvester.js +12 -4
- package/dist/esm/common/payloads/payloads.js +118 -0
- package/dist/esm/common/serialize/bel-serializer.js +4 -3
- package/dist/esm/common/util/data-size.js +2 -2
- package/dist/esm/common/util/obfuscate.js +147 -3
- package/dist/esm/common/wrap/wrap-websocket.js +2 -1
- 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 +52 -14
- package/dist/esm/features/ajax/constants.js +5 -0
- package/dist/esm/features/ajax/instrument/index.js +106 -18
- package/dist/esm/features/generic_events/aggregate/index.js +15 -10
- package/dist/esm/features/generic_events/constants.js +3 -2
- package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
- package/dist/esm/features/jserrors/aggregate/index.js +12 -3
- package/dist/esm/features/logging/aggregate/index.js +7 -3
- package/dist/esm/features/metrics/aggregate/index.js +5 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +7 -3
- package/dist/esm/features/page_view_timing/aggregate/index.js +6 -1
- package/dist/esm/features/session_replay/aggregate/index.js +7 -1
- package/dist/esm/features/session_trace/aggregate/index.js +6 -2
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +36 -5
- package/dist/esm/features/soft_navigations/aggregate/index.js +7 -1
- package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +6 -4
- package/dist/esm/features/utils/aggregate-base.js +2 -3
- 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/constants/events.d.ts +16 -0
- package/dist/types/common/constants/events.d.ts.map +1 -0
- package/dist/types/common/harvest/harvester.d.ts +4 -1
- package/dist/types/common/harvest/harvester.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/common/util/obfuscate.d.ts +23 -2
- package/dist/types/common/util/obfuscate.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-websocket.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 +2 -0
- 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/generic_events/aggregate/index.d.ts +3 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/constants.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +8 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/logging/aggregate/index.d.ts +3 -1
- package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts +2 -0
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -0
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +2 -0
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +2 -0
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +6 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -0
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +0 -1
- package/dist/types/features/utils/aggregate-base.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/constants/events.js +20 -0
- package/src/common/harvest/harvester.js +11 -4
- package/src/common/payloads/payloads.js +135 -0
- package/src/common/serialize/bel-serializer.js +4 -3
- package/src/common/util/data-size.js +2 -2
- package/src/common/util/obfuscate.js +154 -3
- package/src/common/wrap/wrap-websocket.js +2 -1
- 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 +43 -13
- package/src/features/ajax/constants.js +5 -1
- package/src/features/ajax/instrument/index.js +114 -18
- package/src/features/generic_events/aggregate/index.js +14 -9
- package/src/features/generic_events/constants.js +3 -2
- package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
- package/src/features/jserrors/aggregate/index.js +12 -3
- package/src/features/logging/aggregate/index.js +7 -6
- package/src/features/metrics/aggregate/index.js +5 -1
- package/src/features/page_view_event/aggregate/index.js +6 -2
- package/src/features/page_view_timing/aggregate/index.js +6 -1
- package/src/features/session_replay/aggregate/index.js +7 -1
- package/src/features/session_trace/aggregate/index.js +6 -2
- package/src/features/soft_navigations/aggregate/ajax-node.js +26 -5
- package/src/features/soft_navigations/aggregate/index.js +7 -1
- package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/src/features/soft_navigations/aggregate/interaction.js +6 -4
- package/src/features/utils/aggregate-base.js +1 -3
|
@@ -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 { isFileProtocol } from '../url/protocol';
|
|
@@ -10,6 +10,7 @@ import { warn } from './console';
|
|
|
10
10
|
* @typedef {object} ObfuscationRule
|
|
11
11
|
* @property {string|RegExp} regex The regular expression to match against in the payload
|
|
12
12
|
* @property {string} [replacement] The string to replace the matched regex with
|
|
13
|
+
* @property {string[]} [eventFilter] An optional list of event types to which this rule should be applied. If not provided, or an empty array, the rule will be applied to all events.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -24,14 +25,33 @@ import { warn } from './console';
|
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
export class Obfuscator {
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} agentRef - Reference to the agent instance
|
|
30
|
+
* @param {string} [eventType] - Optional event type this obfuscator instance handles.
|
|
31
|
+
* If provided, only rules matching this event type (or rules with no eventFilter) will be applied.
|
|
32
|
+
*/
|
|
33
|
+
constructor(agentRef, eventType) {
|
|
28
34
|
this.agentRef = agentRef;
|
|
35
|
+
this.eventType = eventType;
|
|
29
36
|
this.warnedRegexMissing = false;
|
|
30
37
|
this.warnedInvalidRegex = false;
|
|
31
38
|
this.warnedInvalidReplacement = false;
|
|
32
39
|
}
|
|
33
40
|
get obfuscateConfigRules() {
|
|
34
|
-
|
|
41
|
+
const allRules = this.agentRef.init.obfuscate || [];
|
|
42
|
+
|
|
43
|
+
// If this instance has no specific event type, return all rules
|
|
44
|
+
if (!this.eventType) return allRules;
|
|
45
|
+
|
|
46
|
+
// Filter rules to only those that apply to this instance's event type
|
|
47
|
+
return allRules.filter(rule => {
|
|
48
|
+
// If rule has no eventFilter, it applies to all events
|
|
49
|
+
if (!this.#hasValidEventFilter(rule)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// Otherwise, check if this instance's event type matches the rule's filter
|
|
53
|
+
return rule.eventFilter.includes(this.eventType);
|
|
54
|
+
});
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
/**
|
|
@@ -57,6 +77,130 @@ export class Obfuscator {
|
|
|
57
77
|
}, input);
|
|
58
78
|
}
|
|
59
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Traverses an object and obfuscates all string properties.
|
|
82
|
+
* This instance will only apply rules that match its configured event type (if any).
|
|
83
|
+
* For features with mixed event types in their payloads (like generic_events), this will
|
|
84
|
+
* traverse the object and check each object's eventType property against the rules.
|
|
85
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
86
|
+
* @returns {Object|Array} The modified object
|
|
87
|
+
*/
|
|
88
|
+
traverseAndObfuscateEvents(obj) {
|
|
89
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
90
|
+
|
|
91
|
+
// If this instance was configured with a specific event type, obfuscate everything
|
|
92
|
+
// (rules are already filtered in obfuscateConfigRules getter)
|
|
93
|
+
if (this.eventType) {
|
|
94
|
+
this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this));
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For generic obfuscators (no specific event types), check individual eventType properties
|
|
99
|
+
// This path is for features like generic_events that handle multiple event types
|
|
100
|
+
const eventTypesToObfuscate = new Set();
|
|
101
|
+
const globalRules = [];
|
|
102
|
+
const eventSpecificRules = [];
|
|
103
|
+
this.obfuscateConfigRules.forEach(rule => {
|
|
104
|
+
if (this.#hasValidEventFilter(rule)) {
|
|
105
|
+
eventSpecificRules.push(rule);
|
|
106
|
+
rule.eventFilter.forEach(eventType => eventTypesToObfuscate.add(eventType));
|
|
107
|
+
} else {
|
|
108
|
+
globalRules.push(rule);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Optimization: if ALL rules are global (no event-specific rules exist),
|
|
113
|
+
// we can just apply all rules to everything without checking eventType properties
|
|
114
|
+
if (eventSpecificRules.length === 0) {
|
|
115
|
+
this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this));
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// We have a mix of global and event-specific rules:
|
|
120
|
+
// - Apply event-specific rules only to matching eventTypes
|
|
121
|
+
// - Apply global rules to ALL event types
|
|
122
|
+
this.#applyFnWithEventTypeFilter(obj, this.obfuscateString.bind(this), Array.from(eventTypesToObfuscate), globalRules, null);
|
|
123
|
+
return obj;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Recursively applies a function to all string properties in an object.
|
|
128
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
129
|
+
* @param {Function} fn - The function to apply to string properties
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
#applyFnToAllStrings(obj, fn) {
|
|
133
|
+
if (!obj || typeof obj !== 'object') return;
|
|
134
|
+
Object.keys(obj).forEach(property => {
|
|
135
|
+
const value = obj[property];
|
|
136
|
+
if (typeof value === 'object' && value !== null) {
|
|
137
|
+
this.#applyFnToAllStrings(value, fn);
|
|
138
|
+
} else if (typeof value === 'string') {
|
|
139
|
+
obj[property] = fn(value);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Recursively applies a function to string properties based on eventType filtering.
|
|
146
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
147
|
+
* @param {Function} fn - The function to apply to string properties (applies all rules)
|
|
148
|
+
* @param {string[]} eventTypes - Array of event types to apply event-specific obfuscation
|
|
149
|
+
* @param {Array} globalRules - Rules without eventFilter that apply to all events
|
|
150
|
+
* @param {boolean|null} shouldObfuscate - Track obfuscation state: null = not determined yet, true = obfuscate with all rules, false = obfuscate with global rules only
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
#applyFnWithEventTypeFilter(obj, fn, eventTypes, globalRules, shouldObfuscate) {
|
|
154
|
+
if (!obj || typeof obj !== 'object') return;
|
|
155
|
+
|
|
156
|
+
// Determine the obfuscation state for this object
|
|
157
|
+
let currentShouldObfuscate = shouldObfuscate;
|
|
158
|
+
|
|
159
|
+
// Check if this object has an eventType property
|
|
160
|
+
if ('eventType' in obj && typeof obj.eventType === 'string') {
|
|
161
|
+
currentShouldObfuscate = eventTypes.includes(obj.eventType);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Process all properties
|
|
165
|
+
Object.keys(obj).forEach(property => {
|
|
166
|
+
const value = obj[property];
|
|
167
|
+
if (typeof value === 'object' && value !== null) {
|
|
168
|
+
// Recursively traverse objects/arrays, passing the current obfuscation state
|
|
169
|
+
this.#applyFnWithEventTypeFilter(value, fn, eventTypes, globalRules, currentShouldObfuscate);
|
|
170
|
+
} else if (typeof value === 'string') {
|
|
171
|
+
if (currentShouldObfuscate === true) {
|
|
172
|
+
// Apply all rules (both global and event-specific)
|
|
173
|
+
obj[property] = fn(value);
|
|
174
|
+
} else if (currentShouldObfuscate === false && globalRules.length > 0) {
|
|
175
|
+
// Apply only global rules
|
|
176
|
+
obj[property] = this.#applyRulesToString(value, globalRules);
|
|
177
|
+
}
|
|
178
|
+
// If currentShouldObfuscate is null, we haven't found an eventType yet, so don't obfuscate
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Applies specific obfuscation rules to a string.
|
|
185
|
+
* @param {string} input - The string to obfuscate
|
|
186
|
+
* @param {Array} rules - The rules to apply
|
|
187
|
+
* @returns {string} The obfuscated string
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
#applyRulesToString(input, rules) {
|
|
191
|
+
if (typeof input !== 'string' || input.trim().length === 0) return input;
|
|
192
|
+
const validatedRules = rules.map(rule => this.validateObfuscationRule(rule));
|
|
193
|
+
return validatedRules.filter(ruleValidation => ruleValidation.isValid).reduce((input, ruleValidation) => {
|
|
194
|
+
const {
|
|
195
|
+
rule
|
|
196
|
+
} = ruleValidation;
|
|
197
|
+
return input.replace(rule.regex, rule.replacement || '*');
|
|
198
|
+
}, input);
|
|
199
|
+
}
|
|
200
|
+
#hasValidEventFilter(rule) {
|
|
201
|
+
return Array.isArray(rule?.eventFilter) && rule.eventFilter.length > 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
60
204
|
/**
|
|
61
205
|
* Validates an obfuscation rule and provides errors if any are found.
|
|
62
206
|
* @param {ObfuscationRule} rule The rule to validate
|
|
@@ -8,6 +8,7 @@ import { now } from '../timing/now';
|
|
|
8
8
|
import { cleanURL } from '../url/clean-url';
|
|
9
9
|
import { gosNREUMOriginals } from '../window/nreum';
|
|
10
10
|
import { subscribeToPageUnload } from '../window/page-visibility';
|
|
11
|
+
import { EVENT_TYPES } from '../constants/events';
|
|
11
12
|
const wrapped = {};
|
|
12
13
|
const openWebSockets = new Set(); // track all instances to close out metrics on page unload
|
|
13
14
|
|
|
@@ -33,7 +34,7 @@ export function wrapWebSocket(sharedEE) {
|
|
|
33
34
|
});
|
|
34
35
|
});
|
|
35
36
|
class WrappedWebSocket extends WebSocket {
|
|
36
|
-
static name =
|
|
37
|
+
static name = EVENT_TYPES.WS;
|
|
37
38
|
static toString() {
|
|
38
39
|
// fake native WebSocket when static class is stringified
|
|
39
40
|
return 'function WebSocket() { [native code] }';
|
|
@@ -16,7 +16,7 @@ import { globalScope } from '../constants/runtime';
|
|
|
16
16
|
import { warn } from '../util/console';
|
|
17
17
|
import { findTargetsFromStackTrace } from '../v2/utils';
|
|
18
18
|
const wrapped = {};
|
|
19
|
-
const XHR_PROPS = ['open', 'send']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
19
|
+
const XHR_PROPS = ['open', 'send', 'setRequestHeader']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Wraps the native XMLHttpRequest (XHR) object to emit custom events to its readystatechange event and an assortment
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { isPureObject } from '../../../common/util/type-check';
|
|
@@ -89,4 +89,42 @@ function parseGQLQueryString(gqlQueryString) {
|
|
|
89
89
|
}
|
|
90
90
|
function validateGQLObject(obj) {
|
|
91
91
|
return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks if a response object has valid GraphQL errors.
|
|
96
|
+
* @param {object} response - A single GraphQL response object
|
|
97
|
+
* @returns {boolean} True if the response has valid errors
|
|
98
|
+
*/
|
|
99
|
+
function hasValidGQLErrors(response) {
|
|
100
|
+
return Array.isArray(response?.errors) && response.errors.some(err => err && typeof err === 'object' && typeof err.message === 'string');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Checks if a response body contains GraphQL errors according to the GraphQL spec.
|
|
105
|
+
* A valid GraphQL error response contains an "errors" array with at least one error object.
|
|
106
|
+
* Supports both single and batched GraphQL responses.
|
|
107
|
+
* @param {string|object|array} [responseBody] The response body to check
|
|
108
|
+
* @returns {boolean} True if the response contains GraphQL errors
|
|
109
|
+
*/
|
|
110
|
+
export function hasGQLErrors(responseBody) {
|
|
111
|
+
if (!responseBody) return false;
|
|
112
|
+
try {
|
|
113
|
+
let parsed = responseBody;
|
|
114
|
+
|
|
115
|
+
// Parse string to object if needed
|
|
116
|
+
if (typeof responseBody === 'string') {
|
|
117
|
+
parsed = JSON.parse(responseBody);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle batched GraphQL responses (array of response objects)
|
|
121
|
+
if (Array.isArray(parsed)) {
|
|
122
|
+
return parsed.some(hasValidGQLErrors);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle single GraphQL response
|
|
126
|
+
return hasValidGQLErrors(parsed);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
92
130
|
}
|
|
@@ -6,13 +6,16 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
|
6
6
|
import { stringify } from '../../../common/util/stringify';
|
|
7
7
|
import { handle } from '../../../common/event-emitter/handle';
|
|
8
8
|
import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list';
|
|
9
|
-
import {
|
|
9
|
+
import { FEATURE_NAME, AJAX_ID } from '../constants';
|
|
10
10
|
import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
11
11
|
import { AggregateBase } from '../../utils/aggregate-base';
|
|
12
|
-
import { parseGQL } from './gql';
|
|
13
12
|
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer';
|
|
14
13
|
import { gosNREUMOriginals } from '../../../common/window/nreum';
|
|
14
|
+
import { hasGQLErrors, parseGQL } from './gql';
|
|
15
|
+
import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStringAdders } from '../../../common/payloads/payloads';
|
|
16
|
+
import { Obfuscator } from '../../../common/util/obfuscate';
|
|
15
17
|
import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils';
|
|
18
|
+
import { EVENT_TYPES } from '../../../common/constants/events';
|
|
16
19
|
import { generateUuid } from '../../../common/ids/unique-id';
|
|
17
20
|
export class Aggregate extends AggregateBase {
|
|
18
21
|
static featureName = FEATURE_NAME;
|
|
@@ -20,6 +23,9 @@ export class Aggregate extends AggregateBase {
|
|
|
20
23
|
super(agentRef, FEATURE_NAME);
|
|
21
24
|
setDenyList(agentRef.runtime.denyList);
|
|
22
25
|
const classThis = this;
|
|
26
|
+
|
|
27
|
+
// Create obfuscator for AJAX requests
|
|
28
|
+
this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX);
|
|
23
29
|
if (!agentRef.init.ajax.block_internal) {
|
|
24
30
|
// if the agent is tracking ITSELF, it can spawn endless ajax requests early if they are large from custom attributes, so we just disable early harvest for ajax in this case.
|
|
25
31
|
super.canHarvestEarly = false;
|
|
@@ -84,17 +90,26 @@ export class Aggregate extends AggregateBase {
|
|
|
84
90
|
callbackDuration: metrics.cbTime,
|
|
85
91
|
[AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
86
92
|
};
|
|
93
|
+
event.gql = params.gql = parseGQL({
|
|
94
|
+
body: ctx.requestBody,
|
|
95
|
+
query: ctx.parsedOrigin?.search
|
|
96
|
+
});
|
|
97
|
+
if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = hasGQLErrors(ctx.responseBody);
|
|
98
|
+
const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads;
|
|
99
|
+
const shouldCapturePayload = canCapturePayload(capturePayloadSetting, params.status, event.gql?.operationHasErrors);
|
|
100
|
+
if (shouldCapturePayload) {
|
|
101
|
+
// Store raw data; obfuscation and truncation will happen in the serializer
|
|
102
|
+
params.requestQuery = event.requestQuery = parseQueryString(ctx.parsedOrigin?.search);
|
|
103
|
+
params.requestHeaders = event.requestHeaders = ctx.requestHeaders;
|
|
104
|
+
params.responseHeaders = event.responseHeaders = ctx.responseHeaders;
|
|
105
|
+
if (isLikelyHumanReadable(ctx.requestHeaders, ctx.requestBody)) params.requestBody = event.requestBody = ctx.requestBody;
|
|
106
|
+
if (isLikelyHumanReadable(ctx.responseHeaders, ctx.responseBody)) params.responseBody = event.responseBody = ctx.responseBody;
|
|
107
|
+
}
|
|
87
108
|
if (ctx.dt) {
|
|
88
109
|
event.spanId = ctx.dt.spanId;
|
|
89
110
|
event.traceId = ctx.dt.traceId;
|
|
90
111
|
event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
|
|
91
112
|
}
|
|
92
|
-
|
|
93
|
-
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
94
|
-
event.gql = params.gql = parseGQL({
|
|
95
|
-
body: ctx.body,
|
|
96
|
-
query: ctx.parsedOrigin?.search
|
|
97
|
-
});
|
|
98
113
|
if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length);
|
|
99
114
|
|
|
100
115
|
/** make a copy of the event for the MFE target if it exists */
|
|
@@ -122,7 +137,10 @@ export class Aggregate extends AggregateBase {
|
|
|
122
137
|
}
|
|
123
138
|
serializer(eventBuffer) {
|
|
124
139
|
if (!eventBuffer.length) return;
|
|
125
|
-
const
|
|
140
|
+
const {
|
|
141
|
+
addString,
|
|
142
|
+
addStringWithTruncation
|
|
143
|
+
} = createStringAdders(getAddStringContext, this.obfuscator);
|
|
126
144
|
let payload = 'bel.7;';
|
|
127
145
|
let firstTimestamp = 0;
|
|
128
146
|
for (let i = 0; i < eventBuffer.length; i++) {
|
|
@@ -147,15 +165,35 @@ export class Aggregate extends AggregateBase {
|
|
|
147
165
|
// Since configuration objects (like info) are created new each time they are set, we have to grab the current pointer to the attr object here.
|
|
148
166
|
const jsAttributes = this.agentRef.info.jsAttributes;
|
|
149
167
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
const attrParts = addCustomAttributes({
|
|
168
|
+
// Regular attributes: obfuscate only
|
|
169
|
+
const regularAttrs = addCustomAttributes({
|
|
153
170
|
...(jsAttributes || {}),
|
|
154
|
-
|
|
171
|
+
[AJAX_ID]: event[AJAX_ID],
|
|
172
|
+
// all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
155
173
|
...(event.targetAttributes || {}),
|
|
156
174
|
// used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
157
|
-
|
|
175
|
+
...(event.gql || {})
|
|
158
176
|
}, addString);
|
|
177
|
+
|
|
178
|
+
// Payload attributes: obfuscate then truncate
|
|
179
|
+
const payloadAttrs = addCustomAttributes({
|
|
180
|
+
...(event.requestBody ? {
|
|
181
|
+
requestBody: event.requestBody
|
|
182
|
+
} : {}),
|
|
183
|
+
...(event.requestHeaders ? {
|
|
184
|
+
requestHeaders: event.requestHeaders
|
|
185
|
+
} : {}),
|
|
186
|
+
...(event.requestQuery ? {
|
|
187
|
+
requestQuery: event.requestQuery
|
|
188
|
+
} : {}),
|
|
189
|
+
...(event.responseBody ? {
|
|
190
|
+
responseBody: event.responseBody
|
|
191
|
+
} : {}),
|
|
192
|
+
...(event.responseHeaders ? {
|
|
193
|
+
responseHeaders: event.responseHeaders
|
|
194
|
+
} : {})
|
|
195
|
+
}, addStringWithTruncation);
|
|
196
|
+
const attrParts = [...regularAttrs, ...payloadAttrs];
|
|
159
197
|
fields.unshift(numeric(attrParts.length));
|
|
160
198
|
insert += fields.join(',');
|
|
161
199
|
if (attrParts && attrParts.length > 0) {
|
|
@@ -4,4 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { FEATURE_NAMES } from '../../loaders/features/features';
|
|
6
6
|
export const FEATURE_NAME = FEATURE_NAMES.ajax;
|
|
7
|
+
export const CAPTURE_PAYLOAD_SETTINGS = {
|
|
8
|
+
NONE: 'none',
|
|
9
|
+
FAILURES: 'failures',
|
|
10
|
+
ALL: 'all'
|
|
11
|
+
};
|
|
7
12
|
export const AJAX_ID = 'ajaxRequest.id';
|
|
@@ -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;
|