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.
Files changed (100) hide show
  1. package/README.md +1 -1
  2. package/dist/api/clientConfig.js +225 -217
  3. package/dist/api/helpCode.js +61 -87
  4. package/dist/api/metroplexSocket.js +460 -463
  5. package/dist/api/storedPageVisit.js +150 -208
  6. package/dist/constants.js +10 -2
  7. package/dist/entry/init.js +65 -63
  8. package/dist/monitors/{appNavigationMonitor.js → AppNavigationMonitor.js} +12 -22
  9. package/dist/monitors/ClickMonitor.js +198 -0
  10. package/dist/monitors/ErrorMonitor.js +206 -0
  11. package/dist/monitors/KeyboardInputMonitor.js +60 -0
  12. package/dist/monitors/PageMonitor.js +98 -0
  13. package/dist/monitors/RequestMonitor.js +390 -0
  14. package/dist/monitors/http-tools/GqlErrorValidator.js +259 -0
  15. package/dist/monitors/http-tools/HTTPDataBundler.js +458 -0
  16. package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
  17. package/dist/pageVisit/EventDebouncer.js +99 -0
  18. package/dist/pageVisit/pageVisitEventError.js +2 -2
  19. package/dist/pageVisit/pageVisitEventHTTP.js +79 -93
  20. package/dist/react/ErrorBoundary.js +18 -15
  21. package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
  22. package/dist/sessionRecorder/sessionRecorder.js +152 -151
  23. package/dist/{api → src/api}/clientConfig.d.ts +2 -2
  24. package/dist/{api → src/api}/helpCode.d.ts +10 -16
  25. package/dist/{api → src/api}/metroplexSocket.d.ts +48 -67
  26. package/dist/{api → src/api}/storedPageVisit.d.ts +12 -21
  27. package/dist/{constants.d.ts → src/constants.d.ts} +45 -0
  28. package/dist/{entry → src/entry}/init.d.ts +1 -1
  29. package/dist/src/monitors/AppNavigationMonitor.d.ts +18 -0
  30. package/dist/src/monitors/ClickMonitor.d.ts +31 -0
  31. package/dist/src/monitors/ErrorMonitor.d.ts +63 -0
  32. package/dist/{monitors/keyboardInputMonitor.d.ts → src/monitors/KeyboardInputMonitor.d.ts} +7 -4
  33. package/dist/{monitors/pageMonitor.d.ts → src/monitors/PageMonitor.d.ts} +6 -8
  34. package/dist/src/monitors/RequestMonitor.d.ts +94 -0
  35. package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +59 -0
  36. package/dist/src/monitors/http-tools/HTTPDataBundler.d.ts +112 -0
  37. package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +3 -2
  38. package/dist/src/pageVisit/EventDebouncer.d.ts +24 -0
  39. package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +1 -1
  40. package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
  41. package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
  42. package/dist/{storage → src/storage}/rnStorageProvider.d.ts +1 -1
  43. package/dist/{storage → src/storage}/storage.d.ts +2 -2
  44. package/dist/{storage → src/storage}/storageProvider.d.ts +3 -3
  45. package/dist/{utils → src/utils}/function.d.ts +27 -7
  46. package/dist/{utils → src/utils}/object.d.ts +11 -8
  47. package/dist/src/utils/piiRedactor.d.ts +11 -0
  48. package/dist/src/utils/polyfills.d.ts +4 -0
  49. package/dist/storage/rnStorageProvider.js +7 -4
  50. package/dist/storage/storage.js +43 -35
  51. package/dist/storage/storageProvider.js +23 -19
  52. package/dist/types/Config.d.ts +24 -20
  53. package/dist/types/Metroplex.types.d.ts +73 -0
  54. package/dist/types/Monitor.d.ts +11 -0
  55. package/dist/types/Monitor.js +19 -0
  56. package/dist/types/PageVisit.types.d.ts +8 -0
  57. package/dist/types/PageVisitErrors.types.d.ts +114 -0
  58. package/dist/types/PageVisitEvents.types.d.ts +91 -0
  59. package/dist/types/PageVisitMetrics.types.d.ts +27 -0
  60. package/dist/types/Storage.d.ts +1 -1
  61. package/dist/types/StoredPageVisit.types.d.ts +4 -47
  62. package/dist/types/WrappedObjects.d.ts +6 -0
  63. package/dist/utils/function.js +110 -77
  64. package/dist/utils/object.js +59 -6
  65. package/dist/utils/piiRedactor.js +98 -0
  66. package/dist/utils/polyfills.js +24 -0
  67. package/package.json +8 -8
  68. package/dist/monitors/appNavigationMonitor.d.ts +0 -22
  69. package/dist/monitors/clickMonitor.d.ts +0 -44
  70. package/dist/monitors/clickMonitor.js +0 -251
  71. package/dist/monitors/errorMonitor.d.ts +0 -28
  72. package/dist/monitors/errorMonitor.js +0 -180
  73. package/dist/monitors/gqlErrorValidator.d.ts +0 -82
  74. package/dist/monitors/gqlErrorValidator.js +0 -306
  75. package/dist/monitors/httpDataBundler.d.ts +0 -161
  76. package/dist/monitors/httpDataBundler.js +0 -725
  77. package/dist/monitors/inputMonitor.d.ts +0 -34
  78. package/dist/monitors/inputMonitor.js +0 -138
  79. package/dist/monitors/keyboardInputMonitor.js +0 -66
  80. package/dist/monitors/pageMonitor.js +0 -122
  81. package/dist/monitors/requestMonitor.d.ts +0 -10
  82. package/dist/monitors/requestMonitor.js +0 -401
  83. package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
  84. package/dist/types/PageVisit.d.ts +0 -22
  85. package/dist/types/ReactNative.d.ts +0 -4
  86. package/dist/types/globals.d.ts +0 -45
  87. /package/dist/{api → src/api}/inputManager.d.ts +0 -0
  88. /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
  89. /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
  90. /package/dist/{entry → src/entry}/index.d.ts +0 -0
  91. /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
  92. /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
  93. /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
  94. /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
  95. /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
  96. /package/dist/{utils → src/utils}/date.d.ts +0 -0
  97. /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
  98. /package/dist/{utils → src/utils}/log.d.ts +0 -0
  99. /package/dist/{utils → src/utils}/performance.d.ts +0 -0
  100. /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 };