noibu-react-native 0.0.1

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