noibu-react-native 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/dist/api/clientConfig.js +225 -217
  3. package/dist/api/metroplexSocket.js +406 -416
  4. package/dist/constants.js +14 -2
  5. package/dist/entry/init.js +58 -56
  6. package/dist/monitors/appNavigationMonitor.js +2 -3
  7. package/dist/monitors/clickMonitor.js +16 -9
  8. package/dist/monitors/errorMonitor.js +30 -8
  9. package/dist/monitors/gqlErrorValidator.js +4 -4
  10. package/dist/monitors/httpDataBundler.js +525 -713
  11. package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
  12. package/dist/monitors/requestMonitor.js +350 -365
  13. package/dist/pageVisit/eventDebouncer.js +110 -0
  14. package/dist/pageVisit/pageVisitEventError.js +1 -1
  15. package/dist/pageVisit/pageVisitEventHTTP.js +78 -93
  16. package/dist/react/ErrorBoundary.js +18 -15
  17. package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
  18. package/dist/sessionRecorder/sessionRecorder.js +151 -150
  19. package/dist/{api → src/api}/clientConfig.d.ts +1 -1
  20. package/dist/{api → src/api}/metroplexSocket.d.ts +25 -25
  21. package/dist/{constants.d.ts → src/constants.d.ts} +44 -0
  22. package/dist/{entry → src/entry}/init.d.ts +1 -1
  23. package/dist/{monitors → src/monitors}/clickMonitor.d.ts +1 -1
  24. package/dist/{monitors → src/monitors}/gqlErrorValidator.d.ts +6 -6
  25. package/dist/src/monitors/httpDataBundler.d.ts +127 -0
  26. package/dist/src/monitors/requestMonitor.d.ts +10 -0
  27. package/dist/src/pageVisit/eventDebouncer.d.ts +31 -0
  28. package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
  29. package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
  30. package/dist/{storage → src/storage}/storage.d.ts +1 -1
  31. package/dist/{storage → src/storage}/storageProvider.d.ts +1 -1
  32. package/dist/{utils → src/utils}/function.d.ts +25 -4
  33. package/dist/{utils → src/utils}/object.d.ts +9 -4
  34. package/dist/src/utils/piiRedactor.d.ts +11 -0
  35. package/dist/src/utils/polyfills.d.ts +7 -0
  36. package/dist/storage/rnStorageProvider.js +7 -4
  37. package/dist/storage/storage.js +43 -35
  38. package/dist/storage/storageProvider.js +23 -19
  39. package/dist/types/Config.d.ts +24 -20
  40. package/dist/types/PageVisit.types.d.ts +151 -0
  41. package/dist/types/PageVisitMetrics.types.d.ts +27 -0
  42. package/dist/types/RRWeb.d.ts +48 -0
  43. package/dist/types/StoredPageVisit.types.d.ts +2 -4
  44. package/dist/types/WrappedObjects.d.ts +6 -0
  45. package/dist/utils/function.js +110 -76
  46. package/dist/utils/object.js +58 -6
  47. package/dist/utils/piiRedactor.js +98 -0
  48. package/dist/utils/polyfills.js +24 -0
  49. package/package.json +5 -6
  50. package/dist/monitors/httpDataBundler.d.ts +0 -161
  51. package/dist/monitors/requestMonitor.d.ts +0 -10
  52. package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
  53. package/dist/types/PageVisit.d.ts +0 -22
  54. package/dist/types/globals.d.ts +0 -45
  55. /package/dist/{api → src/api}/helpCode.d.ts +0 -0
  56. /package/dist/{api → src/api}/inputManager.d.ts +0 -0
  57. /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
  58. /package/dist/{api → src/api}/storedPageVisit.d.ts +0 -0
  59. /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
  60. /package/dist/{entry → src/entry}/index.d.ts +0 -0
  61. /package/dist/{monitors → src/monitors}/appNavigationMonitor.d.ts +0 -0
  62. /package/dist/{monitors → src/monitors}/errorMonitor.d.ts +0 -0
  63. /package/dist/{monitors → src/monitors}/inputMonitor.d.ts +0 -0
  64. /package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +0 -0
  65. /package/dist/{monitors → src/monitors}/keyboardInputMonitor.d.ts +0 -0
  66. /package/dist/{monitors → src/monitors}/pageMonitor.d.ts +0 -0
  67. /package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +0 -0
  68. /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
  69. /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
  70. /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
  71. /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
  72. /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
  73. /package/dist/{storage → src/storage}/rnStorageProvider.d.ts +0 -0
  74. /package/dist/{utils → src/utils}/date.d.ts +0 -0
  75. /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
  76. /package/dist/{utils → src/utils}/log.d.ts +0 -0
  77. /package/dist/{utils → src/utils}/performance.d.ts +0 -0
  78. /package/dist/{utils → src/utils}/stacktrace-parser.d.ts +0 -0
@@ -1,725 +1,537 @@
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';
1
+ import { __awaiter } from 'tslib';
2
+ 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, CONTENT_TYPE, CONTENT_LENGTH, BLOCKED_HTTP_HEADER_KEYS, PII_REDACTION_REPLACEMENT_STRING, MAX_HTTP_DATA_PAYLOAD_LENGTH, MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH } from '../constants.js';
2
3
  import ClientConfig from '../api/clientConfig.js';
3
4
  import StoredMetrics from '../api/storedMetrics.js';
4
- import { iterateObjectRecursively } from '../utils/object.js';
5
- import { noibuLog } from '../utils/log.js';
6
-
7
- /** @module HTTPDataBundler */
5
+ import { safeFromEntries, safeEntries } from '../utils/object.js';
6
+ import { safeTrim, stringifyJSON, isString } from '../utils/function.js';
7
+ import { removePII } from '../utils/piiRedactor.js';
8
8
 
9
9
  /** Bundles HTTP payloads and headers */
10
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
- );
11
+ /**
12
+ * Creates an instance of the ClickMonitor instance
13
+ */
14
+ constructor() {
15
+ // compile regex only once
16
+ this.contentTypeReadableRegex = new RegExp(HUMAN_READABLE_CONTENT_TYPE_REGEX, 'i');
17
+ // pull out the domain hostname
18
+ const initialURL = ClientConfig.getInstance().globalUrl;
19
+ this.initialURLPartsReversed = [];
20
+ this.hostname = '';
21
+ if (initialURL && initialURL.length > 0) {
22
+ try {
23
+ // Store URL parts to class variable
24
+ this.hostname = new URL(initialURL).hostname;
25
+ this.initialURLPartsReversed = this.hostname.split('.');
26
+ // Remove www. etc.
27
+ // eslint-disable-next-line no-unused-expressions
28
+ DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(this.initialURLPartsReversed[0]) && this.initialURLPartsReversed.shift();
29
+ // Reverse array
30
+ this.initialURLPartsReversed.reverse();
31
+ }
32
+ catch (e) {
33
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for initial URL: ${e}`, false, SEVERITY.warn);
34
+ }
35
+ }
36
+ this.httpDataCollectionEnabled =
37
+ !!ClientConfig.getInstance().enableHttpDataCollection;
38
+ // compile the relative and full HTTP URL regexes
39
+ const allowedURLs = ClientConfig.getInstance().listOfUrlsToCollectHttpDataFrom;
40
+ this.httpDataAllowedAbsoluteRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, true);
41
+ this.httpDataAllowedRelativeRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, false);
42
+ // track unique requests and only capture each once
43
+ // TODO: disabled for beta. NOI-4253
44
+ // this.uniqueRequests = new Set();
45
+ }
46
+ /** gets the singleton instance
47
+ * @returns {HTTPDataBundler}
48
+ * */
49
+ static getInstance() {
50
+ if (!this.instance) {
51
+ this.instance = new HTTPDataBundler();
52
+ }
53
+ return this.instance;
54
+ }
55
+ /**
56
+ * Builds the HTTP payload allowed regexes for full and relative URLs
57
+ * @param allowedURLs A list of allowed URLs
58
+ * @param absolute Use only absolute URLs if true, use only relative URL if false
59
+ * @returns a regex of allowed URLs
60
+ */
61
+ static buildAllowedRegex(allowedURLs, absolute) {
62
+ if (!allowedURLs || !Array.isArray(allowedURLs))
63
+ return null;
64
+ const allowedURLsFiltered = allowedURLs
65
+ .map(url => safeTrim(url).toLowerCase())
66
+ .filter(url => {
67
+ const isAbsolute = HTTPDataBundler.isAbsoluteURL(url);
68
+ if (absolute) {
69
+ return url && isAbsolute;
70
+ }
71
+ return url && !isAbsolute;
72
+ });
73
+ if (allowedURLsFiltered.length > 0) {
74
+ return new RegExp(allowedURLsFiltered.join('|'));
75
+ }
209
76
  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
77
  }
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;
78
+ /**
79
+ * Takes an iterator and returns a map of strings representing headers.
80
+ * @param {object} headersIterable any iterable object
81
+ * @returns a map of strings (as expected by metroplex) representing HTTP
82
+ * request or response headers
83
+ */
84
+ static headersMapFromIterable(headersIterable) {
85
+ const headerMap = new Map();
86
+ // eslint-disable-next-line no-restricted-syntax
87
+ for (const header of headersIterable) {
88
+ // Ensure we're working with strings
89
+ // This will also automatically convert null to "null"
90
+ if (typeof header[0] !== 'string')
91
+ header[0] = String(header[0]);
92
+ if (typeof header[1] !== 'string')
93
+ header[1] = String(header[1]);
94
+ headerMap.set(header[0].toLowerCase(), header[1]);
95
+ }
96
+ return headerMap;
97
+ }
98
+ /**
99
+ * Takes a string of headers with 'name: value' and returns
100
+ * a map of strings representing headers.
101
+ * headersString is all the headers in one string
102
+ * returns a map of strings (as expected by metroplex) representing HTTP
103
+ * request or response headers
104
+ */
105
+ static headersMapFromString(headersString) {
106
+ const headerMap = new Map();
107
+ if (!headersString || typeof headersString !== 'string')
108
+ return headerMap;
109
+ const headerStringArray = headersString.split('\r\n').filter(Boolean);
110
+ headerStringArray.forEach(function (headerString) {
111
+ const split = headerString.split(': ');
112
+ if (split.length === 2 && split[0].length > 0 && split[1].length > 0) {
113
+ headerMap.set(split[0].toLowerCase(), split[1]);
114
+ }
115
+ });
116
+ return headerMap;
117
+ }
118
+ /**
119
+ * For an XHR object, checks the responseType property and handles the response or
120
+ * responseText property accordingly to return a string representation of the response.
121
+ * @returns a string representation of the response, or null if this fails.
122
+ */
123
+ static getResponseStringFromXHR(xhr) {
124
+ return __awaiter(this, void 0, void 0, function* () {
125
+ var _a;
126
+ // This should not happen, as this function is called from a method on xhr.
127
+ if (!xhr)
128
+ return null;
129
+ if (xhr.responseType === '' || xhr.responseType === 'text') {
130
+ return xhr.responseText;
131
+ }
132
+ // If the XHR object exists but the response is null, return null as a string.
133
+ if (!xhr.response)
134
+ return HTTP_BODY_NULL_STRING;
135
+ if ((_a = xhr.response.documentElement) === null || _a === void 0 ? void 0 : _a.innerHTML) {
136
+ return xhr.response.documentElement.innerHTML;
137
+ }
138
+ if (typeof xhr.response.text === 'function') {
139
+ return yield xhr.response.text();
140
+ }
141
+ if (xhr.responseType === 'json') {
142
+ try {
143
+ const text = stringifyJSON(xhr.response);
144
+ if (text === '{}')
145
+ return null; // stringifyJSON returns {} of it fails to serialize a property
146
+ return text;
147
+ }
148
+ catch (e) {
149
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify JSON response: ${e}`, false, SEVERITY.warn);
150
+ return null;
151
+ }
152
+ }
153
+ return null;
154
+ });
155
+ }
156
+ /**
157
+ * Takes a URL and returns true if it is determined to be on the same domain as the URL
158
+ * the script is running on. Ignores protocol and path, and allows the URL to be a subdomain.
159
+ * @param {string} requestURL the URL of a request to compare to the script website's URL
160
+ * @param {bool} isHostnameCheck the URL is a domain hostname, could be a super or sub domain
161
+ */
162
+ isURLSameDomain(requestURL, isHostnameCheck = false) {
163
+ if (typeof requestURL !== 'string' ||
164
+ !this.initialURLPartsReversed ||
165
+ this.initialURLPartsReversed.length < 1) {
166
+ return false;
167
+ }
168
+ let requestHostname = isHostnameCheck ? requestURL : ''; // has to be declared outside of `try`
169
+ // Use URL constructor to extract hostname
170
+ if (!isHostnameCheck) {
171
+ try {
172
+ let requestURLWithProtocol = requestURL;
173
+ if (requestURL.startsWith('//')) {
174
+ // eslint-disable-next-line prefer-template
175
+ requestURLWithProtocol = 'https:' + requestURL;
176
+ }
177
+ requestHostname = new URL(requestURLWithProtocol).hostname;
178
+ }
179
+ catch (e) {
180
+ // Return false if URL() is unable to parse the request URL
181
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for request URL: ${e}`, false, SEVERITY.warn);
182
+ return false;
183
+ }
184
+ }
185
+ const requestParts = requestHostname.split('.');
186
+ // Remove first subdomain from either URL if it is www., www1., etc.
187
+ if (requestParts.length < 1)
188
+ return false;
189
+ /* eslint-disable no-unused-expressions */
190
+ DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(requestParts[0]) &&
191
+ requestParts.shift();
192
+ /* eslint-enable no-unused-expressions */
193
+ // Reverse order array
194
+ requestParts.reverse();
195
+ if (!isHostnameCheck &&
196
+ requestParts.length < this.initialURLPartsReversed.length)
197
+ return false;
198
+ // Return true if for every part in the website part, the same part exists
199
+ // in the request part, or if alsoCheckSuperDomain is true.
200
+ if (isHostnameCheck) {
201
+ const isSubDomain = requestParts.length >= this.initialURLPartsReversed.length &&
202
+ this.initialURLPartsReversed.every((part, i) => part === requestParts[i]);
203
+ const isSuperDomain = requestParts.length <= this.initialURLPartsReversed.length &&
204
+ requestParts.every((part, i) => part === this.initialURLPartsReversed[i]);
205
+ return isSubDomain || isSuperDomain;
206
+ }
207
+ // Default return false if alsoCheckSuperDomain is not true
208
+ return this.initialURLPartsReversed.every((part, i) => part === requestParts[i]);
209
+ }
210
+ /**
211
+ * Builds an HTTP Data bundle
212
+ */
213
+ bundleHTTPData(url, requestHeaders, rawRequestPayload, responseHeaders, rawResponsePayload, method, isError) {
214
+ if (!this.isValidRequest(url, method)) {
215
+ return null;
216
+ }
217
+ // stringify payload if correct type and not too large
218
+ let requestPayload = '';
219
+ let responsePayload = '';
220
+ if (this.shouldCollectPayloadForURL(url)) {
221
+ requestPayload =
222
+ this.getReasonPayloadIsDropped(requestHeaders, isError) ||
223
+ this.stringFromRequestBody(rawRequestPayload, requestHeaders);
224
+ responsePayload =
225
+ this.getReasonPayloadIsDropped(responseHeaders, isError) ||
226
+ this.stringFromRequestBody(rawResponsePayload, responseHeaders);
227
+ }
228
+ // don't bundle if there is no data
229
+ const safeRequestHeaders = requestHeaders || new Map();
230
+ const safeRequestPayload = requestPayload || '';
231
+ const safeResponseHeaders = responseHeaders || new Map();
232
+ const safeResponsePayload = responsePayload || '';
233
+ if (safeRequestHeaders.size === 0 &&
234
+ !safeRequestPayload &&
235
+ safeResponseHeaders.size === 0 &&
236
+ !safeResponsePayload) {
237
+ return null;
238
+ }
239
+ // Ensure payloads do not exceed the maximum size and redact PII
240
+ const requestPayloadUpdated = this.restrictPayload(safeRequestPayload, url, isError);
241
+ // Ensure payloads do not exceed the maximum size and redact PII
242
+ const responsePayloadUpdated = this.restrictPayload(safeResponsePayload, url, isError);
243
+ // Redact PII.
244
+ const cleanRequestHeaders = safeFromEntries(this.removePIIHeaders(requestHeaders));
245
+ const cleanResponseHeaders = safeFromEntries(this.removePIIHeaders(responseHeaders));
246
+ return {
247
+ [HTTP_DATA_REQ_HEADERS_ATT_NAME]: cleanRequestHeaders,
248
+ [HTTP_DATA_PAYLOAD_ATT_NAME]: requestPayloadUpdated,
249
+ [HTTP_DATA_RESP_HEADERS_ATT_NAME]: cleanResponseHeaders,
250
+ [HTTP_DATA_RESP_PAYLOAD_ATT_NAME]: responsePayloadUpdated,
251
+ };
252
+ }
253
+ /**
254
+ * Validates a request based on the URL and method. When enabled, will handle
255
+ * de-duping the requests
256
+ * @param {string} url
257
+ * @param {string} method
258
+ * @returns boolean indicating whether the validation passed
259
+ */
260
+ isValidRequest(url, method) {
261
+ if (!this.httpDataCollectionEnabled)
262
+ return false;
263
+ if (!method || typeof method !== 'string') {
264
+ return false;
265
+ }
266
+ // TODO: Check below disabled for beta. NOI-4253
267
+ // only bundle and send once per request/method/response code combination
268
+ // const urlWithoutParams = url.split('?')[0];
269
+ // const requestSignature = `${urlWithoutParams}-${method}-${responseCode}`;
270
+ // if (this.uniqueRequests.has(requestSignature)) {
271
+ // return false;
272
+ // }
273
+ return true;
274
+ }
275
+ /**
276
+ * Checks two things: that the URL is either on the same domain (or an address relative to the
277
+ * current domain), and also checks that the config http_data_collection flag is enabled.
278
+ * @param {string} url
279
+ * @returns boolean indicating whether the URL passed is either relative (in which case it is
280
+ * inherently on the current domain) or matches the current domain.
281
+ */
282
+ shouldContinueForURL(url) {
283
+ if (!this.httpDataCollectionEnabled)
284
+ return false;
285
+ if (!url || typeof url !== 'string' || !this.initialURLPartsReversed) {
286
+ return false;
287
+ }
288
+ // URL is absolute; either "http://example.com" or "//example.com"
289
+ if (HTTPDataBundler.isAbsoluteURL(url)) {
290
+ // Only capture requests on the same domain or in the allowed list
291
+ if (!this.isURLSameDomain(url) && !this.shouldCollectPayloadForURL(url))
292
+ return false;
293
+ } // end `if` (if URL is relative, it is on the same domain.)
294
+ return true;
295
+ }
296
+ /**
297
+ * Determins if the URL is absolute or relative
298
+ * @param {string} url
299
+ * @returns boolean indicating whether the URL passed is either absolute or relative
300
+ */
301
+ static isAbsoluteURL(url) {
302
+ if (!url || typeof url !== 'string') {
303
+ return false;
304
+ }
305
+ return url.indexOf('://') > 0 || url.indexOf('//') === 0;
306
+ }
307
+ /**
308
+ * Checks whether HTTP payloads can be collected on this URL
309
+ * @returns boolean indicating whether HTTP payloads can be collected on this URL
310
+ */
311
+ shouldCollectPayloadForURL(url) {
312
+ if (!url || typeof url !== 'string') {
313
+ return false;
314
+ }
315
+ // check if in the full URL allowed list
316
+ const isAllowedAsAbsolute = !!this.httpDataAllowedAbsoluteRegex &&
317
+ this.httpDataAllowedAbsoluteRegex.test(url.toLowerCase());
318
+ const isRelative = !HTTPDataBundler.isAbsoluteURL(url) || this.isURLSameDomain(url);
319
+ // check if in the relative URL allowed list or if it is on the same domain
320
+ const isAllowedAsRelative =
321
+ // if this is a relative URL (isURLSameDomain will fail) OR a full URL on domain
322
+ isRelative &&
323
+ !!this.httpDataAllowedRelativeRegex &&
324
+ this.httpDataAllowedRelativeRegex.test(url.toLowerCase());
325
+ return isAllowedAsAbsolute || isAllowedAsRelative;
326
+ }
327
+ /**
328
+ * Double checks content length if we couldn't read the headers, and redacts PII
329
+ * returns the restricted payload
330
+ */
331
+ restrictPayload(payload, url, isError) {
332
+ // if no payload, too large, payload already dropped,
333
+ // or payloads not allowed for this URL: nothing to do
334
+ if (!payload || !this.shouldCollectPayloadForURL(url)) {
335
+ return HTTP_BODY_NULL_STRING;
336
+ }
337
+ if (typeof payload !== 'string') {
338
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient({
339
+ msg: `restrictPayload received non string payload`,
340
+ payloadType: typeof payload,
341
+ }, false, SEVERITY.error);
342
+ return HTTP_BODY_NULL_STRING;
343
+ }
344
+ if (payload === HTTP_BODY_NULL_STRING ||
345
+ (!!payload.startsWith &&
346
+ (payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
347
+ payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG))) ||
348
+ (!!payload.indexOf &&
349
+ (payload.indexOf(HTTP_BODY_DROPPED_LENGTH_MSG) === 0 ||
350
+ payload.indexOf(HTTP_BODY_DROPPED_TYPE_MSG) === 0))) {
351
+ return payload;
352
+ }
353
+ let restrictSize = isError
354
+ ? MAX_HTTP_DATA_PAYLOAD_LENGTH
355
+ : MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
356
+ // TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
357
+ if (this.hostname === 'www.holtrenfrew.com') {
358
+ restrictSize = 1.5 * 1024 * 1024;
359
+ }
360
+ if (payload.length > restrictSize) {
361
+ StoredMetrics.getInstance().addHttpDataDropByLength();
362
+ return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${payload.length}`;
363
+ }
364
+ // payload is good!
365
+ StoredMetrics.getInstance().addHttpDataPayloadCount();
366
+ return removePII(payload);
367
+ }
368
+ /**
369
+ * Returns true if the content-length header is of acceptable size.
370
+ * Too big gets rejected
371
+ * If the headers are not found, check actual content for length
372
+ * @param {Headers} headers
373
+ * @returns boolean true if acceptable to collect
374
+ */
375
+ contentLengthAcceptable(headers, isError) {
376
+ // TODO This entire check should move into the parent's parent so there is a single place
377
+ // that restricts paylods and unterstands these 2 values
378
+ let restrictSize = isError
379
+ ? MAX_HTTP_DATA_PAYLOAD_LENGTH
380
+ : MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
381
+ // TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
382
+ if (this.hostname === 'www.holtrenfrew.com') {
383
+ restrictSize = 1.5 * 1024 * 1024;
384
+ }
385
+ return this.contentLength(headers) <= restrictSize;
386
+ }
387
+ /**
388
+ * Returns true if the content type according to the headers is valid for collection.
389
+ * Also returns assumed true if content type is not stated in headers.
390
+ * @param {Map} headersMap
391
+ * @returns boolean true if acceptable to collect
392
+ */
393
+ contentTypeAcceptable(headersMap) {
394
+ // check content type
395
+ const contentType = headersMap.get(CONTENT_TYPE);
396
+ return !(contentType &&
397
+ !this.contentTypeReadableRegex.test(contentType.toLowerCase()));
398
+ }
399
+ /**
400
+ * Returns a descriptive string if we have to drop payload based on the length
401
+ * or type listed in the headers passed. Returns an empty string otherwise.
402
+ */
403
+ getReasonPayloadIsDropped(headers, isError) {
404
+ if (!(headers === null || headers === void 0 ? void 0 : headers.get)) {
405
+ return '';
406
+ }
407
+ const bundler = HTTPDataBundler.getInstance();
408
+ if (!bundler.contentTypeAcceptable(headers)) {
409
+ // not a readable request, drop the content
410
+ StoredMetrics.getInstance().addHttpDataDropByType();
411
+ return `${HTTP_BODY_DROPPED_TYPE_MSG} Payload type: ${headers.get(CONTENT_TYPE)}`;
412
+ }
413
+ if (!bundler.contentLengthAcceptable(headers, isError)) {
414
+ StoredMetrics.getInstance().addHttpDataDropByLength();
415
+ return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${bundler.contentLength(headers)}`;
416
+ }
417
+ return '';
418
+ }
419
+ /**
420
+ * Returns content length from the headers, if available.
421
+ * If headers are not found or length is not a number, return -1
422
+ */
423
+ contentLength(headersObject) {
424
+ if (!(headersObject === null || headersObject === void 0 ? void 0 : headersObject.get)) {
425
+ return 0;
426
+ }
427
+ // get content length
428
+ let contentLength = 0;
429
+ const contentValueString = headersObject.get(CONTENT_LENGTH);
430
+ if (!contentValueString) {
431
+ // no content length header found
432
+ return -1;
433
+ }
434
+ try {
435
+ contentLength = parseInt(contentValueString, 10);
436
+ if (Number.isNaN(contentLength)) {
437
+ // bad content length header
438
+ return -1;
439
+ }
440
+ }
441
+ catch (err) {
442
+ // if we can't convert the value to a number, confirm after reading payload
443
+ return -1;
444
+ }
445
+ return contentLength;
446
+ }
447
+ /**
448
+ * Accepts a value that could be any type used as a request payload,
449
+ * and returns a string representation, or null if this fails.
450
+ */
451
+ stringFromRequestBody(value, requestHeaders) {
452
+ /* eslint-disable no-param-reassign */
453
+ // Nullish check to ensure we don't send 'null' or 'undefined' as a string
454
+ if (value == null)
455
+ return null;
456
+ try {
457
+ if (isString(value) && requestHeaders instanceof Map) {
458
+ const contentType = requestHeaders.get(CONTENT_TYPE);
459
+ if (contentType &&
460
+ contentType
461
+ .toLowerCase()
462
+ .includes('application/x-www-form-urlencoded')) {
463
+ value = new URLSearchParams(value.toString());
464
+ }
465
+ }
466
+ }
467
+ catch (error) {
468
+ // Nothing--try next try block
469
+ }
470
+ try {
471
+ // If the payload is a FormData object, turn it into an object.
472
+ if (value instanceof FormData || value instanceof URLSearchParams) {
473
+ value = safeEntries(value).reduce((obj, [key, val]) => {
474
+ obj[key] = this.stringFromRequestBody(val);
475
+ return obj;
476
+ }, {});
477
+ }
478
+ }
479
+ catch (error) {
480
+ // Nothing--try next try block
481
+ }
482
+ try {
483
+ // Case where value has a .toString() method
484
+ const returnVal = value.toString();
485
+ // Catch case where .toString() is on an object, we don't want that --
486
+ // should be stringified below.
487
+ if (!returnVal.includes('[object'))
488
+ return returnVal;
489
+ }
490
+ catch (error) {
491
+ // Nothing--try next try block
492
+ }
493
+ try {
494
+ // Case where value is a Document type (HTML/XML)
495
+ return value.documentElement.innerHTML;
496
+ }
497
+ catch (error) {
498
+ // Nothing--try next try block
499
+ }
500
+ try {
501
+ // Case where value is any other object type
502
+ return stringifyJSON(value);
503
+ }
504
+ catch (e) {
505
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify request body: ${e}`, false, SEVERITY.warn);
506
+ }
507
+ return null;
698
508
  }
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;
509
+ /**
510
+ * Removes possible PII from headers.
511
+ * @param {Map} dirtyHeaders a Map of HTTP response or request headers
512
+ * @returns {Map|null} a map of headers with PII redacted
513
+ */
514
+ removePIIHeaders(dirtyHeaders) {
515
+ // In order to be fail-safe, let's return null if we won't be able to remove PII headers.
516
+ if (!(dirtyHeaders instanceof Map)) {
517
+ return null;
518
+ }
519
+ if (!dirtyHeaders.size) {
520
+ return dirtyHeaders;
521
+ }
522
+ const cleanHeaders = new Map(dirtyHeaders);
523
+ cleanHeaders.forEach((headerVal, headerKey, map) => {
524
+ // If the key is known PII, redact
525
+ if (BLOCKED_HTTP_HEADER_KEYS.includes(headerKey.toLowerCase())) {
526
+ map.set(headerKey, PII_REDACTION_REPLACEMENT_STRING);
527
+ // If not, run the header's value through the string redaction function
528
+ }
529
+ else {
530
+ map.set(headerKey, removePII(headerVal));
531
+ }
532
+ });
533
+ return cleanHeaders;
721
534
  }
722
- }
723
535
  }
724
536
 
725
537
  export { HTTPDataBundler };