noibu-react-native 0.2.2 → 0.2.4
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/README.md +1 -1
- package/dist/api/clientConfig.js +225 -217
- package/dist/api/helpCode.js +61 -87
- package/dist/api/metroplexSocket.js +460 -463
- package/dist/api/storedPageVisit.js +150 -208
- package/dist/constants.js +10 -2
- package/dist/entry/init.js +65 -63
- package/dist/monitors/{appNavigationMonitor.js → AppNavigationMonitor.js} +12 -22
- package/dist/monitors/ClickMonitor.js +198 -0
- package/dist/monitors/ErrorMonitor.js +206 -0
- package/dist/monitors/KeyboardInputMonitor.js +60 -0
- package/dist/monitors/PageMonitor.js +98 -0
- package/dist/monitors/RequestMonitor.js +390 -0
- package/dist/monitors/http-tools/GqlErrorValidator.js +259 -0
- package/dist/monitors/http-tools/HTTPDataBundler.js +458 -0
- package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
- package/dist/pageVisit/EventDebouncer.js +99 -0
- package/dist/pageVisit/pageVisitEventError.js +2 -2
- package/dist/pageVisit/pageVisitEventHTTP.js +79 -93
- package/dist/react/ErrorBoundary.js +18 -15
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
- package/dist/sessionRecorder/sessionRecorder.js +152 -151
- package/dist/{api → src/api}/clientConfig.d.ts +2 -2
- package/dist/{api → src/api}/helpCode.d.ts +10 -16
- package/dist/{api → src/api}/metroplexSocket.d.ts +48 -67
- package/dist/{api → src/api}/storedPageVisit.d.ts +12 -21
- package/dist/{constants.d.ts → src/constants.d.ts} +45 -0
- package/dist/{entry → src/entry}/init.d.ts +1 -1
- package/dist/src/monitors/AppNavigationMonitor.d.ts +18 -0
- package/dist/src/monitors/ClickMonitor.d.ts +31 -0
- package/dist/src/monitors/ErrorMonitor.d.ts +63 -0
- package/dist/{monitors/keyboardInputMonitor.d.ts → src/monitors/KeyboardInputMonitor.d.ts} +7 -4
- package/dist/{monitors/pageMonitor.d.ts → src/monitors/PageMonitor.d.ts} +6 -8
- package/dist/src/monitors/RequestMonitor.d.ts +94 -0
- package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +59 -0
- package/dist/src/monitors/http-tools/HTTPDataBundler.d.ts +112 -0
- package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +3 -2
- package/dist/src/pageVisit/EventDebouncer.d.ts +24 -0
- package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +1 -1
- package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
- package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
- package/dist/{storage → src/storage}/rnStorageProvider.d.ts +1 -1
- package/dist/{storage → src/storage}/storage.d.ts +2 -2
- package/dist/{storage → src/storage}/storageProvider.d.ts +3 -3
- package/dist/{utils → src/utils}/function.d.ts +27 -7
- package/dist/{utils → src/utils}/object.d.ts +11 -8
- package/dist/src/utils/piiRedactor.d.ts +11 -0
- package/dist/src/utils/polyfills.d.ts +4 -0
- package/dist/storage/rnStorageProvider.js +7 -4
- package/dist/storage/storage.js +43 -35
- package/dist/storage/storageProvider.js +23 -19
- package/dist/types/Config.d.ts +24 -20
- package/dist/types/Metroplex.types.d.ts +73 -0
- package/dist/types/Monitor.d.ts +11 -0
- package/dist/types/Monitor.js +19 -0
- package/dist/types/PageVisit.types.d.ts +8 -0
- package/dist/types/PageVisitErrors.types.d.ts +114 -0
- package/dist/types/PageVisitEvents.types.d.ts +91 -0
- package/dist/types/PageVisitMetrics.types.d.ts +27 -0
- package/dist/types/Storage.d.ts +1 -1
- package/dist/types/StoredPageVisit.types.d.ts +4 -47
- package/dist/types/WrappedObjects.d.ts +6 -0
- package/dist/utils/function.js +110 -77
- package/dist/utils/object.js +59 -6
- package/dist/utils/piiRedactor.js +98 -0
- package/dist/utils/polyfills.js +24 -0
- package/package.json +8 -8
- package/dist/monitors/appNavigationMonitor.d.ts +0 -22
- package/dist/monitors/clickMonitor.d.ts +0 -44
- package/dist/monitors/clickMonitor.js +0 -251
- package/dist/monitors/errorMonitor.d.ts +0 -28
- package/dist/monitors/errorMonitor.js +0 -180
- package/dist/monitors/gqlErrorValidator.d.ts +0 -82
- package/dist/monitors/gqlErrorValidator.js +0 -306
- package/dist/monitors/httpDataBundler.d.ts +0 -161
- package/dist/monitors/httpDataBundler.js +0 -725
- package/dist/monitors/inputMonitor.d.ts +0 -34
- package/dist/monitors/inputMonitor.js +0 -138
- package/dist/monitors/keyboardInputMonitor.js +0 -66
- package/dist/monitors/pageMonitor.js +0 -122
- package/dist/monitors/requestMonitor.d.ts +0 -10
- package/dist/monitors/requestMonitor.js +0 -401
- package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
- package/dist/types/PageVisit.d.ts +0 -22
- package/dist/types/ReactNative.d.ts +0 -4
- package/dist/types/globals.d.ts +0 -45
- /package/dist/{api → src/api}/inputManager.d.ts +0 -0
- /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
- /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
- /package/dist/{entry → src/entry}/index.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
- /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
- /package/dist/{utils → src/utils}/date.d.ts +0 -0
- /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
- /package/dist/{utils → src/utils}/log.d.ts +0 -0
- /package/dist/{utils → src/utils}/performance.d.ts +0 -0
- /package/dist/{utils → src/utils}/stacktrace-parser.d.ts +0 -0
|
@@ -1,725 +0,0 @@
|
|
|
1
|
-
import { HUMAN_READABLE_CONTENT_TYPE_REGEX, DEFAULT_WEBSITE_SUBDOMAIN_PATTERN, SEVERITY, HTTP_BODY_NULL_STRING, HTTP_DATA_REQ_HEADERS_ATT_NAME, HTTP_DATA_PAYLOAD_ATT_NAME, HTTP_DATA_RESP_HEADERS_ATT_NAME, HTTP_DATA_RESP_PAYLOAD_ATT_NAME, HTTP_BODY_DROPPED_LENGTH_MSG, HTTP_BODY_DROPPED_TYPE_MSG, MAX_HTTP_DATA_PAYLOAD_LENGTH, CONTENT_TYPE, CONTENT_LENGTH, BLOCKED_HTTP_HEADER_KEYS, PII_REDACTION_REPLACEMENT_STRING, HTTP_PII_BLOCKING_PATTERNS } from '../constants.js';
|
|
2
|
-
import ClientConfig from '../api/clientConfig.js';
|
|
3
|
-
import StoredMetrics from '../api/storedMetrics.js';
|
|
4
|
-
import { iterateObjectRecursively } from '../utils/object.js';
|
|
5
|
-
import { noibuLog } from '../utils/log.js';
|
|
6
|
-
|
|
7
|
-
/** @module HTTPDataBundler */
|
|
8
|
-
|
|
9
|
-
/** Bundles HTTP payloads and headers */
|
|
10
|
-
class HTTPDataBundler {
|
|
11
|
-
/**
|
|
12
|
-
* Creates an instance of the ClickMonitor instance
|
|
13
|
-
*/
|
|
14
|
-
constructor() {
|
|
15
|
-
// compile regex only once
|
|
16
|
-
this.contentTypeReadableRegex = new RegExp(
|
|
17
|
-
HUMAN_READABLE_CONTENT_TYPE_REGEX,
|
|
18
|
-
'i',
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
// pull out the domain hostname
|
|
22
|
-
const initialURL = ClientConfig.getInstance().globalUrl;
|
|
23
|
-
this.initialURLPartsReversed = [];
|
|
24
|
-
if (initialURL && initialURL.length > 0) {
|
|
25
|
-
try {
|
|
26
|
-
// Store URL parts to class variable
|
|
27
|
-
const initialHostname = new URL(initialURL).hostname;
|
|
28
|
-
this.initialURLPartsReversed = initialHostname.split('.');
|
|
29
|
-
|
|
30
|
-
// Remove www. etc.
|
|
31
|
-
// eslint-disable-next-line no-unused-expressions
|
|
32
|
-
DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(
|
|
33
|
-
this.initialURLPartsReversed[0],
|
|
34
|
-
) && this.initialURLPartsReversed.shift();
|
|
35
|
-
|
|
36
|
-
// Reverse array
|
|
37
|
-
this.initialURLPartsReversed.reverse();
|
|
38
|
-
} catch (e) {
|
|
39
|
-
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
40
|
-
`Unable to determine hostname for initial URL: ${e}`,
|
|
41
|
-
false,
|
|
42
|
-
SEVERITY.warn,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
this.httpDataCollectionEnabled =
|
|
48
|
-
!!ClientConfig.getInstance().enableHttpDataCollection;
|
|
49
|
-
|
|
50
|
-
// compile the relative and full HTTP URL regexes
|
|
51
|
-
const allowedURLs = HTTPDataBundler.getHttpPayloadAllowedURLs();
|
|
52
|
-
this.httpDataAllowedAbsoluteRegex = HTTPDataBundler.buildAllowedRegex(
|
|
53
|
-
allowedURLs,
|
|
54
|
-
'absolute',
|
|
55
|
-
);
|
|
56
|
-
this.httpDataAllowedRelativeRegex = HTTPDataBundler.buildAllowedRegex(
|
|
57
|
-
allowedURLs,
|
|
58
|
-
'relative',
|
|
59
|
-
);
|
|
60
|
-
// track unique requests and only capture each once
|
|
61
|
-
// TODO: disabled for beta. NOI-4253
|
|
62
|
-
// this.uniqueRequests = new Set();
|
|
63
|
-
|
|
64
|
-
this.fuzzyFieldsToRedact = [
|
|
65
|
-
'password',
|
|
66
|
-
'address',
|
|
67
|
-
'credit',
|
|
68
|
-
'postal',
|
|
69
|
-
'token',
|
|
70
|
-
'phone',
|
|
71
|
-
'mobile',
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
this.exactFieldsToRedact = [
|
|
75
|
-
'firstname',
|
|
76
|
-
'lastname',
|
|
77
|
-
'street',
|
|
78
|
-
'fullname',
|
|
79
|
-
'creditcard',
|
|
80
|
-
'postcode',
|
|
81
|
-
'zipcode',
|
|
82
|
-
'city',
|
|
83
|
-
'town',
|
|
84
|
-
'county',
|
|
85
|
-
'cc',
|
|
86
|
-
];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** gets http data payload allowed URLs */
|
|
90
|
-
static getHttpPayloadAllowedURLs() {
|
|
91
|
-
const noibuConfig = ClientConfig.getInstance();
|
|
92
|
-
// return the allowed list or an empty list
|
|
93
|
-
if (
|
|
94
|
-
noibuConfig.listOfUrlsToCollectHttpDataFrom &&
|
|
95
|
-
Array.isArray(noibuConfig.listOfUrlsToCollectHttpDataFrom)
|
|
96
|
-
) {
|
|
97
|
-
return noibuConfig.listOfUrlsToCollectHttpDataFrom;
|
|
98
|
-
}
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* gets the singleton instance
|
|
104
|
-
* @returns {HTTPDataBundler}
|
|
105
|
-
*/
|
|
106
|
-
static getInstance() {
|
|
107
|
-
if (!this.instance) {
|
|
108
|
-
this.instance = new HTTPDataBundler();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return this.instance;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Builds the HTTP payload allowed regexes for full and relative URLs
|
|
116
|
-
* @param allowedURLs A list of allowed URLs
|
|
117
|
-
* @param {'absolute' | 'relative'} strategy Use only absolute URLs if true, use only relative URL if false
|
|
118
|
-
* @returns a regex of allowed URLs
|
|
119
|
-
*/
|
|
120
|
-
static buildAllowedRegex(allowedURLs, strategy) {
|
|
121
|
-
if (!allowedURLs) return null;
|
|
122
|
-
const allowedURLsFiltered = allowedURLs.filter(url => {
|
|
123
|
-
const isAbsolute = HTTPDataBundler.isAbsoluteURL(url);
|
|
124
|
-
return strategy === 'absolute' ? isAbsolute : !isAbsolute;
|
|
125
|
-
});
|
|
126
|
-
if (allowedURLsFiltered.length > 0) {
|
|
127
|
-
const lowerCasedURLs = allowedURLsFiltered.map(url =>
|
|
128
|
-
url.trim().toLowerCase(),
|
|
129
|
-
);
|
|
130
|
-
return new RegExp(lowerCasedURLs.join('|'));
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Takes an iterator and returns a map of strings representing headers.
|
|
137
|
-
* @param {object} headersIterable any iterable object
|
|
138
|
-
* @returns a map of strings (as expected by metroplex) representing HTTP
|
|
139
|
-
* request or response headers
|
|
140
|
-
*/
|
|
141
|
-
static headersMapFromIterable(headersIterable) {
|
|
142
|
-
const headerMap = new Map();
|
|
143
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
144
|
-
for (const header of headersIterable) {
|
|
145
|
-
// Ensure we're working with strings
|
|
146
|
-
// This will also automatically convert null to "null"
|
|
147
|
-
if (typeof header[0] !== 'string') header[0] = String(header[0]);
|
|
148
|
-
if (typeof header[1] !== 'string') header[1] = String(header[1]);
|
|
149
|
-
|
|
150
|
-
headerMap.set(header[0].toLowerCase(), header[1]);
|
|
151
|
-
}
|
|
152
|
-
return headerMap;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Takes a string of headers with 'name: value' and returns
|
|
157
|
-
* a map of strings representing headers.
|
|
158
|
-
* @param {string} headersString is all the headers in one string
|
|
159
|
-
* @returns a map of strings (as expected by metroplex) representing HTTP
|
|
160
|
-
* request or response headers
|
|
161
|
-
*/
|
|
162
|
-
static headersMapFromString(headersString) {
|
|
163
|
-
const headerMap = new Map();
|
|
164
|
-
if (!headersString || typeof headersString !== 'string') return headerMap;
|
|
165
|
-
const headerStringArray = headersString.split('\r\n').filter(Boolean);
|
|
166
|
-
headerStringArray.forEach(headerString => {
|
|
167
|
-
const split = headerString.split(': ');
|
|
168
|
-
if (split.length === 2 && split[0].length > 0 && split[1].length > 0) {
|
|
169
|
-
headerMap.set(split[0].toLowerCase(), split[1]);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
return headerMap;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* For an XHR object, checks the responseType property and handles the response or
|
|
177
|
-
* responseText property accordingly to return a string representation of the response.
|
|
178
|
-
* @param {object} xhr an XMLHTTPRequest object
|
|
179
|
-
* @returns a string representation of the response, or null if this fails.
|
|
180
|
-
*/
|
|
181
|
-
static responseStringFromXHRResponseType(xhr) {
|
|
182
|
-
// This should not happen, as this function is called from a method on xhr.
|
|
183
|
-
if (xhr === undefined || xhr === null) return null;
|
|
184
|
-
|
|
185
|
-
// If the XHR object exists but the response is null, return null as a string.
|
|
186
|
-
// noinspection PointlessBooleanExpressionJS
|
|
187
|
-
if (xhr.response && xhr.response === null) return HTTP_BODY_NULL_STRING;
|
|
188
|
-
|
|
189
|
-
if (xhr.responseType === '' || xhr.responseType === 'text') {
|
|
190
|
-
return xhr.responseText;
|
|
191
|
-
}
|
|
192
|
-
if (
|
|
193
|
-
xhr.responseType === 'document' &&
|
|
194
|
-
!!xhr.response &&
|
|
195
|
-
!!xhr.response.documentElement &&
|
|
196
|
-
!!xhr.response.documentElement.innerHTML
|
|
197
|
-
) {
|
|
198
|
-
return xhr.response.documentElement.innerHTML;
|
|
199
|
-
}
|
|
200
|
-
if (xhr.responseType === 'json') {
|
|
201
|
-
try {
|
|
202
|
-
return JSON.stringify(xhr.response);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
205
|
-
`Unable to stringify JSON response: ${e}`,
|
|
206
|
-
false,
|
|
207
|
-
SEVERITY.warn,
|
|
208
|
-
);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Takes a URL and returns true if it is determined to be on the same domain as the URL
|
|
217
|
-
* the script is running on. Ignores protocol and path, and allows the URL to be a subdomain.
|
|
218
|
-
* @param {string} requestURL the URL of a request to compare to the script website's URL
|
|
219
|
-
*/
|
|
220
|
-
isURLSameDomain(requestURL) {
|
|
221
|
-
if (
|
|
222
|
-
typeof requestURL !== 'string' ||
|
|
223
|
-
!this.initialURLPartsReversed ||
|
|
224
|
-
this.initialURLPartsReversed.length < 1
|
|
225
|
-
) {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let requestHostname; // has to be declared outside `try`
|
|
230
|
-
|
|
231
|
-
// Use URL constructor to extract hostname
|
|
232
|
-
try {
|
|
233
|
-
requestHostname = new URL(requestURL).hostname;
|
|
234
|
-
} catch (e) {
|
|
235
|
-
// Return false if URL() is unable to parse the request URL
|
|
236
|
-
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
237
|
-
`Unable to determine hostname for request URL: ${e}`,
|
|
238
|
-
false,
|
|
239
|
-
SEVERITY.warn,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const requestParts = requestHostname.split('.');
|
|
246
|
-
|
|
247
|
-
// Remove first subdomain from either URL if it is www., www1., etc.
|
|
248
|
-
|
|
249
|
-
if (requestParts.length < 1) return false;
|
|
250
|
-
|
|
251
|
-
/* eslint-disable no-unused-expressions */
|
|
252
|
-
DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(requestParts[0]) &&
|
|
253
|
-
requestParts.shift();
|
|
254
|
-
/* eslint-enable no-unused-expressions */
|
|
255
|
-
|
|
256
|
-
if (requestParts.length < this.initialURLPartsReversed.length) return false;
|
|
257
|
-
|
|
258
|
-
// Reverse order array
|
|
259
|
-
requestParts.reverse();
|
|
260
|
-
|
|
261
|
-
// Return true if for every part in the website part, the same part exists
|
|
262
|
-
// in the request part.
|
|
263
|
-
return this.initialURLPartsReversed.every(
|
|
264
|
-
(part, i) => part === requestParts[i],
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Builds an HTTP Data bundle
|
|
270
|
-
* @param url
|
|
271
|
-
* @param requestHeaders is a map of request headers
|
|
272
|
-
* @param rawRequestPayload
|
|
273
|
-
* @param responseHeaders is a map of response headers
|
|
274
|
-
* @param responsePayload
|
|
275
|
-
* @param method
|
|
276
|
-
* @param shouldCollectPayload
|
|
277
|
-
*/
|
|
278
|
-
bundleHTTPData(
|
|
279
|
-
url,
|
|
280
|
-
requestHeaders,
|
|
281
|
-
rawRequestPayload,
|
|
282
|
-
responseHeaders,
|
|
283
|
-
responsePayload,
|
|
284
|
-
method,
|
|
285
|
-
shouldCollectPayload,
|
|
286
|
-
) {
|
|
287
|
-
noibuLog('bundleHTTPData started for', method, url);
|
|
288
|
-
if (!this.isValidRequest(url, method)) {
|
|
289
|
-
noibuLog('quitting');
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// stringify payload if correct type and not too large
|
|
294
|
-
let requestPayload = '';
|
|
295
|
-
if (shouldCollectPayload) {
|
|
296
|
-
noibuLog('bundleHTTPData collecting payload');
|
|
297
|
-
requestPayload =
|
|
298
|
-
this.dropPayloadIfNecessaryFromHeaders(requestHeaders) ||
|
|
299
|
-
this.stringFromRequestBody(rawRequestPayload);
|
|
300
|
-
} else {
|
|
301
|
-
noibuLog('bundleHTTPData not collecting payload');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// don't bundle if there is no data
|
|
305
|
-
const safeRequestHeaders = requestHeaders || new Map();
|
|
306
|
-
const safeRequestPayload = requestPayload || '';
|
|
307
|
-
const safeResponseHeaders = responseHeaders || new Map();
|
|
308
|
-
const safeResponsePayload = responsePayload || '';
|
|
309
|
-
if (
|
|
310
|
-
safeRequestHeaders.size === 0 &&
|
|
311
|
-
safeRequestPayload.length === 0 &&
|
|
312
|
-
safeResponseHeaders.size === 0 &&
|
|
313
|
-
safeResponsePayload.length === 0
|
|
314
|
-
) {
|
|
315
|
-
noibuLog('bundleHTTPData payload is empty, quitting');
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Ensure payloads do not exceed the maximum size and redact PII
|
|
320
|
-
const requestPayloadUpdated = this.restrictPayload(requestPayload, url);
|
|
321
|
-
// Ensure payloads do not exceed the maximum size and redact PII
|
|
322
|
-
const responsePayloadUpdated = this.restrictPayload(responsePayload, url);
|
|
323
|
-
|
|
324
|
-
// Redact PII.
|
|
325
|
-
const cleanRequestHeaders = this.removePIIHeaders(requestHeaders);
|
|
326
|
-
const cleanResponseHeaders = this.removePIIHeaders(responseHeaders);
|
|
327
|
-
|
|
328
|
-
// TODO: disabled for beta. NOI-4253
|
|
329
|
-
// we are capturing this request, so add it to the unique requests set
|
|
330
|
-
// this.uniqueRequests.add(requestSignature);
|
|
331
|
-
// build the http bundle
|
|
332
|
-
return {
|
|
333
|
-
[HTTP_DATA_REQ_HEADERS_ATT_NAME]: cleanRequestHeaders
|
|
334
|
-
? Object.fromEntries(cleanRequestHeaders)
|
|
335
|
-
: {},
|
|
336
|
-
[HTTP_DATA_PAYLOAD_ATT_NAME]: requestPayloadUpdated,
|
|
337
|
-
[HTTP_DATA_RESP_HEADERS_ATT_NAME]: cleanResponseHeaders
|
|
338
|
-
? Object.fromEntries(cleanResponseHeaders)
|
|
339
|
-
: {},
|
|
340
|
-
[HTTP_DATA_RESP_PAYLOAD_ATT_NAME]: responsePayloadUpdated,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Validates a request based on the URL and method. When enabled, will handle
|
|
346
|
-
* de-duping the requests
|
|
347
|
-
* @param {string} url
|
|
348
|
-
* @param {string} method
|
|
349
|
-
* @returns boolean indicating whether the validation passed
|
|
350
|
-
*/
|
|
351
|
-
isValidRequest(url, method) {
|
|
352
|
-
noibuLog('isValidRequest');
|
|
353
|
-
if (!this.httpDataCollectionEnabled) {
|
|
354
|
-
noibuLog('isValidRequest httpDataCollectionEnabled is off, quitting');
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
if (!method || typeof method !== 'string') {
|
|
358
|
-
noibuLog('isValidRequest method is invalid, quitting');
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
// TODO: Check below disabled for beta. NOI-4253
|
|
362
|
-
// only bundle and send once per request/method/response code combination
|
|
363
|
-
// const urlWithoutParams = url.split('?')[0];
|
|
364
|
-
// const requestSignature = `${urlWithoutParams}-${method}-${responseCode}`;
|
|
365
|
-
// if (this.uniqueRequests.has(requestSignature)) {
|
|
366
|
-
// return false;
|
|
367
|
-
// }
|
|
368
|
-
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Checks two things: that the URL is either on the same domain (or an address relative to the
|
|
374
|
-
* current domain), and also checks that the config http_data_collection flag is enabled.
|
|
375
|
-
* @param {string} url
|
|
376
|
-
* @returns boolean indicating whether the URL passed is either relative (in which case it is
|
|
377
|
-
* inherently on the current domain) or matches the current domain.
|
|
378
|
-
*/
|
|
379
|
-
shouldContinueForURL(url) {
|
|
380
|
-
noibuLog('shouldContinueForURL started for', url);
|
|
381
|
-
if (!this.httpDataCollectionEnabled) {
|
|
382
|
-
noibuLog('shouldContinueForURL httpDataCollection turned off, quitting');
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
if (!url || typeof url !== 'string' || !this.initialURLPartsReversed) {
|
|
386
|
-
noibuLog('shouldContinueForURL url is not valid, quitting', {
|
|
387
|
-
initialUrlPartsReversed: this.initialURLPartsReversed,
|
|
388
|
-
});
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
// URL is absolute; either "http://example.com" or "//example.com"
|
|
392
|
-
if (HTTPDataBundler.isAbsoluteURL(url)) {
|
|
393
|
-
// Only capture requests on the same domain or in the allowed list
|
|
394
|
-
if (!this.isURLSameDomain(url) && !this.shouldCollectPayloadForURL(url)) {
|
|
395
|
-
noibuLog(
|
|
396
|
-
'shouldContinueForURL url is not same domain or is not in allow list, quitting',
|
|
397
|
-
{
|
|
398
|
-
shouldCollectPayloadForURL: this.shouldCollectPayloadForURL(url),
|
|
399
|
-
},
|
|
400
|
-
);
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
} // end `if` (if URL is relative, it is on the same domain.)
|
|
404
|
-
|
|
405
|
-
noibuLog('shouldContinueForURL - yes');
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Determins if the URL is absolute or relative
|
|
411
|
-
* @param {string} url
|
|
412
|
-
* @returns boolean indicating whether the URL passed is either absolute or relative
|
|
413
|
-
*/
|
|
414
|
-
static isAbsoluteURL(url) {
|
|
415
|
-
if (!url || typeof url !== 'string') {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
return url.indexOf('://') > 0 || url.indexOf('//') === 0;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Checks whether HTTP payloads can be collected on this URL
|
|
423
|
-
* @param {string} url
|
|
424
|
-
* @returns boolean indicating whether HTTP payloads can be collected on this URL
|
|
425
|
-
*/
|
|
426
|
-
shouldCollectPayloadForURL(url) {
|
|
427
|
-
noibuLog('shouldCollectPayloadForURL', url);
|
|
428
|
-
if (!url || typeof url !== 'string') {
|
|
429
|
-
noibuLog('shouldCollectPayloadForURL url is invalid, quitting');
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
// check if in the full URL allowed list
|
|
433
|
-
if (
|
|
434
|
-
this.httpDataAllowedAbsoluteRegex &&
|
|
435
|
-
this.httpDataAllowedAbsoluteRegex.test(url.toLowerCase())
|
|
436
|
-
) {
|
|
437
|
-
noibuLog('shouldCollectPayloadForURL yes, absolute');
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
// check if in the relative URL allowed list, if on domain
|
|
441
|
-
if (
|
|
442
|
-
this.httpDataAllowedRelativeRegex &&
|
|
443
|
-
// if this is a relative URL (isURLSameDomain will fail) OR a full URL on domain
|
|
444
|
-
(!HTTPDataBundler.isAbsoluteURL(url) || this.isURLSameDomain(url)) &&
|
|
445
|
-
this.httpDataAllowedRelativeRegex.test(url.toLowerCase())
|
|
446
|
-
) {
|
|
447
|
-
noibuLog('shouldCollectPayloadForURL yes, relative');
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
noibuLog('shouldCollectPayloadForURL answer is no, reason', {
|
|
452
|
-
httpDataAllowedAbsoluteRegexTest:
|
|
453
|
-
this.httpDataAllowedAbsoluteRegex &&
|
|
454
|
-
this.httpDataAllowedAbsoluteRegex.test(url.toLowerCase()),
|
|
455
|
-
httpDataAllowedRelativeRegexTest:
|
|
456
|
-
this.httpDataAllowedRelativeRegex &&
|
|
457
|
-
this.httpDataAllowedRelativeRegex.test(url.toLowerCase()),
|
|
458
|
-
isAbsoluteURL: HTTPDataBundler.isAbsoluteURL(url),
|
|
459
|
-
isURLSameDomain: this.isURLSameDomain(url),
|
|
460
|
-
});
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Double checks content length if we couldn't read the headers, and redacts PII
|
|
466
|
-
* @param {string} payload
|
|
467
|
-
* @param {string} url
|
|
468
|
-
* @returns the restricted payload
|
|
469
|
-
*/
|
|
470
|
-
restrictPayload(payload, url) {
|
|
471
|
-
// if no payload, or too large, payload already dropped,
|
|
472
|
-
// or payloads not allowed for this URL: nothing to do
|
|
473
|
-
if (!payload || !this.shouldCollectPayloadForURL(url)) {
|
|
474
|
-
return HTTP_BODY_NULL_STRING;
|
|
475
|
-
}
|
|
476
|
-
if (
|
|
477
|
-
payload === HTTP_BODY_NULL_STRING ||
|
|
478
|
-
payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
|
|
479
|
-
payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG)
|
|
480
|
-
) {
|
|
481
|
-
return payload;
|
|
482
|
-
}
|
|
483
|
-
if (payload.length > MAX_HTTP_DATA_PAYLOAD_LENGTH) {
|
|
484
|
-
StoredMetrics.getInstance().addHttpDataDropByLength();
|
|
485
|
-
return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${payload.length}`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// payload is good!
|
|
489
|
-
StoredMetrics.getInstance().addHttpDataPayloadCount();
|
|
490
|
-
|
|
491
|
-
return this.removePIIBody(payload);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Returns true if the content length is acceptable to collect
|
|
496
|
-
* If the headers are not found, check actual content for length
|
|
497
|
-
* @param {Headers} headers
|
|
498
|
-
* @returns boolean true if acceptable to collect
|
|
499
|
-
*/
|
|
500
|
-
contentLengthAcceptable(headers) {
|
|
501
|
-
// check content length
|
|
502
|
-
const contentLength = this.contentLength(headers);
|
|
503
|
-
if (contentLength < 0) {
|
|
504
|
-
// no content length or can't read, keep going
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
if (contentLength > MAX_HTTP_DATA_PAYLOAD_LENGTH) {
|
|
508
|
-
StoredMetrics.getInstance().addHttpDataDropByLength();
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
return true;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Returns true if the content type according to the headers is valid for collection.
|
|
516
|
-
* Also returns assumed true if content type is not stated in headers.
|
|
517
|
-
* @param {Map} headersMap
|
|
518
|
-
* @returns boolean true if acceptable to collect
|
|
519
|
-
*/
|
|
520
|
-
contentTypeAcceptable(headersMap) {
|
|
521
|
-
if (!headersMap || !headersMap.get) return true;
|
|
522
|
-
|
|
523
|
-
// check content type
|
|
524
|
-
const contentType = headersMap.get(CONTENT_TYPE);
|
|
525
|
-
if (
|
|
526
|
-
contentType &&
|
|
527
|
-
!this.contentTypeReadableRegex.test(contentType.toLowerCase())
|
|
528
|
-
) {
|
|
529
|
-
// not a readable request, drop the content
|
|
530
|
-
StoredMetrics.getInstance().addHttpDataDropByType();
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
return true;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Returns a descriptive string if we have to drop payload based on the length
|
|
538
|
-
* or type listed in the headers passed. Returns an empty string otherwise.
|
|
539
|
-
* @param {Headers} headers a Headers object
|
|
540
|
-
* @returns A string, which is empty if the payload doesn't need to be dropped, or is a
|
|
541
|
-
* descriptive string explaining the circumstances of the drop otherwise.
|
|
542
|
-
*/
|
|
543
|
-
dropPayloadIfNecessaryFromHeaders(headers) {
|
|
544
|
-
// Set descriptive body if content is wrong type or too long.
|
|
545
|
-
let text = '';
|
|
546
|
-
if (!HTTPDataBundler.getInstance().contentTypeAcceptable(headers)) {
|
|
547
|
-
text = HTTP_BODY_DROPPED_TYPE_MSG;
|
|
548
|
-
if (headers && headers.get) {
|
|
549
|
-
text += ` Payload type: ${headers.get(CONTENT_TYPE)}`;
|
|
550
|
-
}
|
|
551
|
-
} else if (
|
|
552
|
-
!HTTPDataBundler.getInstance().contentLengthAcceptable(headers)
|
|
553
|
-
) {
|
|
554
|
-
text = `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: \
|
|
555
|
-
${HTTPDataBundler.getInstance().contentLength(headers)}`;
|
|
556
|
-
}
|
|
557
|
-
return text;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Returns content length from the headers, if available
|
|
562
|
-
* If the headers are not found or not a number, return -1
|
|
563
|
-
* @param {} headersObject
|
|
564
|
-
* @returns content length
|
|
565
|
-
*/
|
|
566
|
-
contentLength(headersObject) {
|
|
567
|
-
if (!headersObject || !headersObject.get) return 0;
|
|
568
|
-
|
|
569
|
-
// get content length
|
|
570
|
-
let contentLength = 0;
|
|
571
|
-
const contentValueString = headersObject.get(CONTENT_LENGTH);
|
|
572
|
-
// no content length header found
|
|
573
|
-
if (!contentValueString) {
|
|
574
|
-
return -1;
|
|
575
|
-
}
|
|
576
|
-
try {
|
|
577
|
-
contentLength = parseInt(contentValueString, 10);
|
|
578
|
-
if (Number.isNaN(contentLength)) {
|
|
579
|
-
// bad content length heade
|
|
580
|
-
return -1;
|
|
581
|
-
}
|
|
582
|
-
} catch (err) {
|
|
583
|
-
// if we can't convert the value to a number, confirm after reading payload
|
|
584
|
-
return -1;
|
|
585
|
-
}
|
|
586
|
-
return contentLength;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Accepts a value that could be any type used as a request payload, and returns a string
|
|
591
|
-
* representation. First attemtps to find a .toString() method, then tries to handle it
|
|
592
|
-
* like an XML or HTML element, then finally falls back to JSON.stringify(). If none of
|
|
593
|
-
* these are successful, returns null.
|
|
594
|
-
* @param {*} value request paylad body, of any type
|
|
595
|
-
* @returns string representation of the value passed, or null if this fails.
|
|
596
|
-
*/
|
|
597
|
-
stringFromRequestBody(value) {
|
|
598
|
-
// Nullish check to ensure we don't send 'null' or 'undefined' as a string
|
|
599
|
-
if (value == null) return null;
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
// Case where value has a .toString() method
|
|
603
|
-
const returnVal = value.toString();
|
|
604
|
-
// Catch case where .toString() is on an object, we don't want that --
|
|
605
|
-
// should be stringified below.
|
|
606
|
-
if (!returnVal.includes('[object')) return returnVal;
|
|
607
|
-
} catch (error) {
|
|
608
|
-
// Nothing--try next try block
|
|
609
|
-
}
|
|
610
|
-
try {
|
|
611
|
-
// Case where value is a Document type (HTML/XML)
|
|
612
|
-
return value.documentElement.innerHTML;
|
|
613
|
-
} catch (error) {
|
|
614
|
-
// Nothing--try next try block
|
|
615
|
-
}
|
|
616
|
-
try {
|
|
617
|
-
// If the payload is a FormData object, turn it into an object.
|
|
618
|
-
if (value instanceof FormData) {
|
|
619
|
-
// eslint-disable-next-line no-param-reassign
|
|
620
|
-
value = Array.from(value.entries()).reduce((obj, [key, val]) => {
|
|
621
|
-
// eslint-disable-next-line no-param-reassign
|
|
622
|
-
obj[key] = typeof val === 'string' ? val : 'non-string value.';
|
|
623
|
-
return obj;
|
|
624
|
-
}, {});
|
|
625
|
-
}
|
|
626
|
-
// Case where value is any other object type
|
|
627
|
-
return JSON.stringify(value);
|
|
628
|
-
} catch (e) {
|
|
629
|
-
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
630
|
-
`Unable to stringify request body: ${e}`,
|
|
631
|
-
false,
|
|
632
|
-
SEVERITY.warn,
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Removes possible PII from headers.
|
|
640
|
-
* @param {Map} dirtyHeaders a Map of HTTP response or request headers
|
|
641
|
-
* @returns a map of headers with PII redacted
|
|
642
|
-
*/
|
|
643
|
-
removePIIHeaders(dirtyHeaders) {
|
|
644
|
-
// In order to be fail-safe, let's return null if we won't be able to remove PII headers.
|
|
645
|
-
if (!(dirtyHeaders instanceof Map)) return null;
|
|
646
|
-
if (dirtyHeaders.size < 1) return dirtyHeaders;
|
|
647
|
-
|
|
648
|
-
const cleanHeaders = new Map(dirtyHeaders);
|
|
649
|
-
|
|
650
|
-
cleanHeaders.forEach((headerVal, headerKey, map) => {
|
|
651
|
-
// If the key is known PII, redact
|
|
652
|
-
if (BLOCKED_HTTP_HEADER_KEYS.includes(headerKey.toLowerCase())) {
|
|
653
|
-
map.set(headerKey, PII_REDACTION_REPLACEMENT_STRING);
|
|
654
|
-
// If not, run the header's value through the string redaction function
|
|
655
|
-
} else {
|
|
656
|
-
map.set(headerKey, this.removePIIBody(headerVal));
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
return cleanHeaders;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Takes a body payload string and redacts any PII we're able to detect
|
|
665
|
-
*
|
|
666
|
-
* @param {string} dirtyBody the string from which we want to redact PII
|
|
667
|
-
* @returns the string with any PII we're able to detect redacted.
|
|
668
|
-
*/
|
|
669
|
-
removePIIBody(dirtyBody) {
|
|
670
|
-
// In order to be fail-safe, let's return null if we won't be able to remove PII.
|
|
671
|
-
if (typeof dirtyBody !== 'string') return null;
|
|
672
|
-
if (dirtyBody.length < 1) return dirtyBody;
|
|
673
|
-
|
|
674
|
-
let cleanString = this.tryParseObjectAndRemovePII(dirtyBody);
|
|
675
|
-
// Go through each of the PII matching patterns
|
|
676
|
-
HTTP_PII_BLOCKING_PATTERNS.forEach(pattern => {
|
|
677
|
-
// Replace all matches with the appropriate number of asterisks
|
|
678
|
-
cleanString = cleanString.replace(
|
|
679
|
-
pattern,
|
|
680
|
-
PII_REDACTION_REPLACEMENT_STRING,
|
|
681
|
-
);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
return cleanString;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Try to parse content as a JSON object and
|
|
689
|
-
* iterate it recursively removing PII.
|
|
690
|
-
* Returns original content if nothing was changes or error is thrown.
|
|
691
|
-
* @param {String} content
|
|
692
|
-
*/
|
|
693
|
-
tryParseObjectAndRemovePII(content) {
|
|
694
|
-
const first = content[0];
|
|
695
|
-
const isParsable = first === '{' || first === '[';
|
|
696
|
-
if (!isParsable) {
|
|
697
|
-
return content;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
let redacted = false;
|
|
701
|
-
try {
|
|
702
|
-
const instance = JSON.parse(content);
|
|
703
|
-
iterateObjectRecursively(instance, (current, property) => {
|
|
704
|
-
const propertyLowerCase = property.toLowerCase();
|
|
705
|
-
// check for exact matches
|
|
706
|
-
if (this.exactFieldsToRedact.some(v => propertyLowerCase === v)) {
|
|
707
|
-
redacted = true;
|
|
708
|
-
return PII_REDACTION_REPLACEMENT_STRING;
|
|
709
|
-
}
|
|
710
|
-
// check for fuzzy matches
|
|
711
|
-
if (this.fuzzyFieldsToRedact.some(v => propertyLowerCase.includes(v))) {
|
|
712
|
-
redacted = true;
|
|
713
|
-
return PII_REDACTION_REPLACEMENT_STRING;
|
|
714
|
-
}
|
|
715
|
-
return undefined;
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
return redacted ? JSON.stringify(instance) : content;
|
|
719
|
-
} catch (e) {
|
|
720
|
-
return content;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export { HTTPDataBundler };
|