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
@@ -0,0 +1,458 @@
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';
3
+ import ClientConfig from '../../api/clientConfig.js';
4
+ import StoredMetrics from '../../api/storedMetrics.js';
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
+ import { Singleton } from '../../types/Monitor.js';
9
+
10
+ /** Bundles HTTP payloads and headers */
11
+ class HTTPDataBundler extends Singleton {
12
+ /**
13
+ * Creates an instance of the ClickMonitor instance
14
+ */
15
+ constructor() {
16
+ super();
17
+ // compile regex only once
18
+ this.contentTypeReadableRegex = new RegExp(HUMAN_READABLE_CONTENT_TYPE_REGEX, 'i');
19
+ // pull out the domain hostname
20
+ const initialURL = ClientConfig.getInstance().globalUrl;
21
+ this.initialURLPartsReversed = [];
22
+ this.hostname = '';
23
+ if (initialURL && initialURL.length > 0) {
24
+ try {
25
+ // Store URL parts to class variable
26
+ this.hostname = new URL(initialURL).hostname;
27
+ this.initialURLPartsReversed = this.hostname.split('.');
28
+ // Remove www. etc.
29
+ // eslint-disable-next-line no-unused-expressions
30
+ DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(this.initialURLPartsReversed[0]) && this.initialURLPartsReversed.shift();
31
+ // Reverse array
32
+ this.initialURLPartsReversed.reverse();
33
+ }
34
+ catch (e) {
35
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for initial URL: ${e}`, false, SEVERITY.warn);
36
+ }
37
+ }
38
+ this.httpDataCollectionEnabled =
39
+ !!ClientConfig.getInstance().enableHttpDataCollection;
40
+ // compile the relative and full HTTP URL regexes
41
+ const allowedURLs = ClientConfig.getInstance().listOfUrlsToCollectHttpDataFrom;
42
+ this.httpDataAllowedAbsoluteRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, true);
43
+ this.httpDataAllowedRelativeRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, false);
44
+ // track unique requests and only capture each once
45
+ // TODO: disabled for beta. NOI-4253
46
+ // this.uniqueRequests = new Set();
47
+ }
48
+ /**
49
+ * Builds the HTTP payload allowed regexes for full and relative URLs
50
+ * @param allowedURLs A list of allowed URLs
51
+ * @param absolute Use only absolute URLs if true, use only relative URL if false
52
+ * @returns a regex of allowed URLs
53
+ */
54
+ static buildAllowedRegex(allowedURLs, absolute) {
55
+ if (!allowedURLs || !Array.isArray(allowedURLs))
56
+ return null;
57
+ const allowedURLsFiltered = allowedURLs
58
+ .map(url => safeTrim(url).toLowerCase())
59
+ .filter(url => {
60
+ const isAbsolute = HTTPDataBundler.isAbsoluteURL(url);
61
+ if (absolute) {
62
+ return url && isAbsolute;
63
+ }
64
+ return url && !isAbsolute;
65
+ });
66
+ if (allowedURLsFiltered.length > 0) {
67
+ return new RegExp(allowedURLsFiltered.join('|'));
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Takes an iterator and returns a map of strings representing headers.
73
+ * param {object} headersIterable any iterable object
74
+ * returns a map of strings (as expected by metroplex) representing HTTP
75
+ * request or response headers
76
+ */
77
+ static headersMapFromIterable(headersIterable) {
78
+ const headerMap = new Map();
79
+ for (const header of headersIterable) {
80
+ // Ensure we're working with strings
81
+ // This will also automatically convert null to "null"
82
+ if (typeof header[0] !== 'string')
83
+ header[0] = String(header[0]);
84
+ if (typeof header[1] !== 'string')
85
+ header[1] = String(header[1]);
86
+ headerMap.set(header[0].toLowerCase(), header[1]);
87
+ }
88
+ return headerMap;
89
+ }
90
+ /**
91
+ * Takes a string of headers with 'name: value' and returns
92
+ * a map of strings representing headers.
93
+ * headersString is all the headers in one string
94
+ * returns a map of strings (as expected by metroplex) representing HTTP
95
+ * request or response headers
96
+ */
97
+ static headersMapFromString(headersString) {
98
+ const headerMap = new Map();
99
+ if (!headersString || typeof headersString !== 'string')
100
+ return headerMap;
101
+ const headerStringArray = headersString.split('\r\n').filter(Boolean);
102
+ headerStringArray.forEach(function (headerString) {
103
+ const split = headerString.split(': ');
104
+ if (split.length === 2 && split[0].length > 0 && split[1].length > 0) {
105
+ headerMap.set(split[0].toLowerCase(), split[1]);
106
+ }
107
+ });
108
+ return headerMap;
109
+ }
110
+ /**
111
+ * For an XHR object, checks the responseType property and handles the response or
112
+ * responseText property accordingly to return a string representation of the response.
113
+ * @returns a string representation of the response, or null if this fails.
114
+ */
115
+ static getResponseStringFromXHR(xhr) {
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ var _a;
118
+ // This should not happen, as this function is called from a method on xhr.
119
+ if (!xhr)
120
+ return null;
121
+ if (xhr.responseType === '' || xhr.responseType === 'text') {
122
+ return xhr.responseText;
123
+ }
124
+ // If the XHR object exists but the response is null, return null as a string.
125
+ if (!xhr.response)
126
+ return HTTP_BODY_NULL_STRING;
127
+ if ((_a = xhr.response.documentElement) === null || _a === void 0 ? void 0 : _a.innerHTML) {
128
+ return xhr.response.documentElement.innerHTML;
129
+ }
130
+ if (typeof xhr.response.text === 'function') {
131
+ return yield xhr.response.text();
132
+ }
133
+ if (xhr.responseType === 'json') {
134
+ try {
135
+ const text = stringifyJSON(xhr.response);
136
+ if (text === '{}')
137
+ return null; // stringifyJSON returns {} of it fails to serialize a property
138
+ return text;
139
+ }
140
+ catch (e) {
141
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify JSON response: ${e}`, false, SEVERITY.warn);
142
+ return null;
143
+ }
144
+ }
145
+ return null;
146
+ });
147
+ }
148
+ /**
149
+ * Builds an HTTP Data bundle
150
+ */
151
+ bundleHTTPData(url, requestHeaders, rawRequestPayload, responseHeaders, rawResponsePayload, method, isError) {
152
+ if (!this.isValidRequest(method)) {
153
+ return null;
154
+ }
155
+ // stringify payload if correct type and not too large
156
+ let requestPayload = '';
157
+ let responsePayload = '';
158
+ if (this.shouldCollectPayloadForURL(url)) {
159
+ requestPayload =
160
+ this.getReasonPayloadIsDropped(requestHeaders, isError) ||
161
+ this.stringFromRequestBody(rawRequestPayload, requestHeaders);
162
+ responsePayload =
163
+ this.getReasonPayloadIsDropped(responseHeaders, isError) ||
164
+ this.stringFromRequestBody(rawResponsePayload, responseHeaders);
165
+ }
166
+ // don't bundle if there is no data
167
+ const safeRequestHeaders = requestHeaders || new Map();
168
+ const safeRequestPayload = requestPayload || '';
169
+ const safeResponseHeaders = responseHeaders || new Map();
170
+ const safeResponsePayload = responsePayload || '';
171
+ if (safeRequestHeaders.size === 0 &&
172
+ !safeRequestPayload &&
173
+ safeResponseHeaders.size === 0 &&
174
+ !safeResponsePayload) {
175
+ return null;
176
+ }
177
+ // Ensure payloads do not exceed the maximum size and redact PII
178
+ const requestPayloadUpdated = this.restrictPayload(safeRequestPayload, url, isError);
179
+ // Ensure payloads do not exceed the maximum size and redact PII
180
+ const responsePayloadUpdated = this.restrictPayload(safeResponsePayload, url, isError);
181
+ // Redact PII.
182
+ const cleanRequestHeaders = safeFromEntries(this.removePIIHeaders(requestHeaders));
183
+ const cleanResponseHeaders = safeFromEntries(this.removePIIHeaders(responseHeaders));
184
+ return {
185
+ [HTTP_DATA_REQ_HEADERS_ATT_NAME]: cleanRequestHeaders,
186
+ [HTTP_DATA_PAYLOAD_ATT_NAME]: requestPayloadUpdated,
187
+ [HTTP_DATA_RESP_HEADERS_ATT_NAME]: cleanResponseHeaders,
188
+ [HTTP_DATA_RESP_PAYLOAD_ATT_NAME]: responsePayloadUpdated,
189
+ };
190
+ }
191
+ /**
192
+ * Validates a request based on the URL and method. When enabled, will handle
193
+ * de-duping the requests
194
+ */
195
+ isValidRequest(method) {
196
+ return (this.httpDataCollectionEnabled && method && typeof method === 'string');
197
+ }
198
+ /**
199
+ * Checks two things: that the URL is either on the same domain (or an address relative to the
200
+ * current domain), and also checks that the config http_data_collection flag is enabled.
201
+ * @param {string} url
202
+ * @returns boolean indicating whether the URL passed is either relative (in which case it is
203
+ * inherently on the current domain) or matches the current domain.
204
+ */
205
+ shouldContinueForURL(url) {
206
+ if (!this.httpDataCollectionEnabled)
207
+ return false;
208
+ if (!url || typeof url !== 'string' || !this.initialURLPartsReversed) {
209
+ return false;
210
+ }
211
+ // URL is absolute; either "http://example.com" or "//example.com"
212
+ if (HTTPDataBundler.isAbsoluteURL(url)) {
213
+ // Only capture requests on the same domain or in the allowed list
214
+ if (!this.shouldCollectPayloadForURL(url)) {
215
+ return false;
216
+ }
217
+ } // end `if` (if URL is relative, it is on the same domain.)
218
+ return true;
219
+ }
220
+ /**
221
+ * Determines if the URL is absolute or relative
222
+ * returns boolean indicating whether the URL passed is either absolute or relative
223
+ */
224
+ static isAbsoluteURL(url) {
225
+ if (!url || typeof url !== 'string') {
226
+ return false;
227
+ }
228
+ return url.indexOf('://') > 0 || url.indexOf('//') === 0;
229
+ }
230
+ /**
231
+ * Checks whether HTTP payloads can be collected on this URL
232
+ * returns boolean indicating whether HTTP payloads can be collected on this URL
233
+ */
234
+ shouldCollectPayloadForURL(url) {
235
+ var _a, _b;
236
+ if (!url || typeof url !== 'string') {
237
+ return false;
238
+ }
239
+ // check if in the full URL allowed list
240
+ const isAllowedAsAbsolute = (_a = this.httpDataAllowedAbsoluteRegex) === null || _a === void 0 ? void 0 : _a.test(url.toLowerCase());
241
+ const isRelative = !HTTPDataBundler.isAbsoluteURL(url);
242
+ // check if in the relative URL allowed list or if it is on the same domain
243
+ const isAllowedAsRelative =
244
+ // if this is a relative URL (isURLSameDomain will fail) OR a full URL on domain
245
+ isRelative && ((_b = this.httpDataAllowedRelativeRegex) === null || _b === void 0 ? void 0 : _b.test(url.toLowerCase()));
246
+ return isAllowedAsAbsolute || isAllowedAsRelative;
247
+ }
248
+ /**
249
+ * Double checks content length if we couldn't read the headers, and redacts PII
250
+ * returns the restricted payload
251
+ */
252
+ restrictPayload(payload, url, isError) {
253
+ // if no payload, too large, payload already dropped,
254
+ // or payloads not allowed for this URL: nothing to do
255
+ if (!payload || !this.shouldCollectPayloadForURL(url)) {
256
+ return HTTP_BODY_NULL_STRING;
257
+ }
258
+ if (typeof payload !== 'string') {
259
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient({
260
+ msg: `restrictPayload received non string payload`,
261
+ payloadType: typeof payload,
262
+ }, false, SEVERITY.error);
263
+ return HTTP_BODY_NULL_STRING;
264
+ }
265
+ if (payload === HTTP_BODY_NULL_STRING ||
266
+ (!!payload.startsWith &&
267
+ (payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
268
+ payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG))) ||
269
+ (!!payload.indexOf &&
270
+ (payload.indexOf(HTTP_BODY_DROPPED_LENGTH_MSG) === 0 ||
271
+ payload.indexOf(HTTP_BODY_DROPPED_TYPE_MSG) === 0))) {
272
+ return payload;
273
+ }
274
+ let restrictSize = isError
275
+ ? MAX_HTTP_DATA_PAYLOAD_LENGTH
276
+ : MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
277
+ // TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
278
+ if (this.hostname === 'www.holtrenfrew.com') {
279
+ restrictSize = 1.5 * 1024 * 1024;
280
+ }
281
+ if (payload.length > restrictSize) {
282
+ StoredMetrics.getInstance().addHttpDataDropByLength();
283
+ return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${payload.length}`;
284
+ }
285
+ // payload is good!
286
+ StoredMetrics.getInstance().addHttpDataPayloadCount();
287
+ return removePII(payload);
288
+ }
289
+ /**
290
+ * Returns true if the content-length header is of acceptable size.
291
+ * Too big gets rejected
292
+ * If the headers are not found, check actual content for length
293
+ * @param {Headers} headers
294
+ * @returns boolean true if acceptable to collect
295
+ */
296
+ contentLengthAcceptable(headers, isError) {
297
+ // TODO This entire check should move into the parent's parent so there is a single place
298
+ // that restricts paylods and unterstands these 2 values
299
+ let restrictSize = isError
300
+ ? MAX_HTTP_DATA_PAYLOAD_LENGTH
301
+ : MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
302
+ // TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
303
+ if (this.hostname === 'www.holtrenfrew.com') {
304
+ restrictSize = 1.5 * 1024 * 1024;
305
+ }
306
+ return this.contentLength(headers) <= restrictSize;
307
+ }
308
+ /**
309
+ * Returns true if the content type according to the headers is valid for collection.
310
+ * Also returns assumed true if content type is not stated in headers.
311
+ * @param {Map} headersMap
312
+ * @returns boolean true if acceptable to collect
313
+ */
314
+ contentTypeAcceptable(headersMap) {
315
+ // check content type
316
+ const contentType = headersMap.get(CONTENT_TYPE);
317
+ return !(contentType &&
318
+ !this.contentTypeReadableRegex.test(contentType.toLowerCase()));
319
+ }
320
+ /**
321
+ * Returns a descriptive string if we have to drop payload based on the length
322
+ * or type listed in the headers passed. Returns an empty string otherwise.
323
+ */
324
+ getReasonPayloadIsDropped(headers, isError) {
325
+ if (!(headers === null || headers === void 0 ? void 0 : headers.get)) {
326
+ return '';
327
+ }
328
+ const bundler = HTTPDataBundler.getInstance();
329
+ if (!bundler.contentTypeAcceptable(headers)) {
330
+ // not a readable request, drop the content
331
+ StoredMetrics.getInstance().addHttpDataDropByType();
332
+ return `${HTTP_BODY_DROPPED_TYPE_MSG} Payload type: ${headers.get(CONTENT_TYPE)}`;
333
+ }
334
+ if (!bundler.contentLengthAcceptable(headers, isError)) {
335
+ StoredMetrics.getInstance().addHttpDataDropByLength();
336
+ return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${bundler.contentLength(headers)}`;
337
+ }
338
+ return '';
339
+ }
340
+ /**
341
+ * Returns content length from the headers, if available.
342
+ * If headers are not found or length is not a number, return -1
343
+ */
344
+ contentLength(headersObject) {
345
+ if (!(headersObject === null || headersObject === void 0 ? void 0 : headersObject.get)) {
346
+ return 0;
347
+ }
348
+ // get content length
349
+ let contentLength = 0;
350
+ const contentValueString = headersObject.get(CONTENT_LENGTH);
351
+ if (!contentValueString) {
352
+ // no content length header found
353
+ return -1;
354
+ }
355
+ try {
356
+ contentLength = parseInt(contentValueString, 10);
357
+ if (Number.isNaN(contentLength)) {
358
+ // bad content length header
359
+ return -1;
360
+ }
361
+ }
362
+ catch (err) {
363
+ // if we can't convert the value to a number, confirm after reading payload
364
+ return -1;
365
+ }
366
+ return contentLength;
367
+ }
368
+ /**
369
+ * Accepts a value that could be any type used as a request payload,
370
+ * and returns a string representation, or null if this fails.
371
+ */
372
+ stringFromRequestBody(value, requestHeaders) {
373
+ /* eslint-disable no-param-reassign */
374
+ // Nullish check to ensure we don't send 'null' or 'undefined' as a string
375
+ if (value == null)
376
+ return null;
377
+ try {
378
+ if (isString(value) && requestHeaders instanceof Map) {
379
+ const contentType = requestHeaders.get(CONTENT_TYPE);
380
+ if (contentType &&
381
+ contentType
382
+ .toLowerCase()
383
+ .includes('application/x-www-form-urlencoded')) {
384
+ value = new URLSearchParams(value.toString());
385
+ }
386
+ }
387
+ }
388
+ catch (error) {
389
+ // Nothing--try next try block
390
+ }
391
+ try {
392
+ // If the payload is a FormData object, turn it into an object.
393
+ if (value instanceof FormData || value instanceof URLSearchParams) {
394
+ value = safeEntries(value).reduce((obj, [key, val]) => {
395
+ obj[key] = this.stringFromRequestBody(val);
396
+ return obj;
397
+ }, {});
398
+ }
399
+ }
400
+ catch (error) {
401
+ // Nothing--try next try block
402
+ }
403
+ try {
404
+ // Case where value has a .toString() method
405
+ const returnVal = value.toString();
406
+ // Catch case where .toString() is on an object, we don't want that --
407
+ // should be stringified below.
408
+ if (!returnVal.includes('[object'))
409
+ return returnVal;
410
+ }
411
+ catch (error) {
412
+ // Nothing--try next try block
413
+ }
414
+ try {
415
+ // Case where value is a Document type (HTML/XML)
416
+ return value.documentElement.innerHTML;
417
+ }
418
+ catch (error) {
419
+ // Nothing--try next try block
420
+ }
421
+ try {
422
+ // Case where value is any other object type
423
+ return stringifyJSON(value);
424
+ }
425
+ catch (e) {
426
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify request body: ${e}`, false, SEVERITY.warn);
427
+ }
428
+ return null;
429
+ }
430
+ /**
431
+ * Removes possible PII from headers.
432
+ * @param {Map} dirtyHeaders a Map of HTTP response or request headers
433
+ * @returns {Map|null} a map of headers with PII redacted
434
+ */
435
+ removePIIHeaders(dirtyHeaders) {
436
+ // In order to be fail-safe, let's return null if we won't be able to remove PII headers.
437
+ if (!(dirtyHeaders instanceof Map)) {
438
+ return null;
439
+ }
440
+ if (!dirtyHeaders.size) {
441
+ return dirtyHeaders;
442
+ }
443
+ const cleanHeaders = new Map(dirtyHeaders);
444
+ cleanHeaders.forEach((headerVal, headerKey, map) => {
445
+ // If the key is known PII, redact
446
+ if (BLOCKED_HTTP_HEADER_KEYS.includes(headerKey.toLowerCase())) {
447
+ map.set(headerKey, PII_REDACTION_REPLACEMENT_STRING);
448
+ // If not, run the header's value through the string redaction function
449
+ }
450
+ else {
451
+ map.set(headerKey, removePII(headerVal));
452
+ }
453
+ });
454
+ return cleanHeaders;
455
+ }
456
+ }
457
+
458
+ export { HTTPDataBundler };
@@ -2,8 +2,10 @@
2
2
  * react-native-navigation adapter
3
3
  */
4
4
  class ReactNativeNavigationIntegration {
5
- stack = [];
6
- stackPointers = {};
5
+ constructor() {
6
+ this.stack = [];
7
+ this.stackPointers = {};
8
+ }
7
9
  /**
8
10
  * attaches provided listeners to the integration
9
11
  */
@@ -0,0 +1,99 @@
1
+ import { PageVisit } from './pageVisit.js';
2
+ import { APP_NAVIGATION_EVENT_TYPE, PAGE_EVENT_TYPE, MAX_TIME_FOR_UNSENT_DATA_MILLIS, ERROR_EVENT_TYPE, HTTP_EVENT_TYPE, KEYBOARD_EVENT_TYPE, SEVERITY } from '../constants.js';
3
+ import { timestampWrapper } from '../utils/date.js';
4
+ import { addSafeEventListener } from '../utils/eventlistener.js';
5
+ import ClientConfig from '../api/clientConfig.js';
6
+ import { Singleton } from '../types/Monitor.js';
7
+
8
+ /** @module EventDebouncer */
9
+ /**
10
+ * Singleton class responsible for debouncing all events
11
+ * that are registered
12
+ */
13
+ class EventDebouncer extends Singleton {
14
+ /**
15
+ * Creates an instance of EventDebouncer
16
+ */
17
+ constructor() {
18
+ super();
19
+ this.eventsToDebounce = {
20
+ [APP_NAVIGATION_EVENT_TYPE]: {
21
+ timeout: null,
22
+ events: [],
23
+ debouncePeriod: 0,
24
+ eventName: APP_NAVIGATION_EVENT_TYPE,
25
+ },
26
+ [PAGE_EVENT_TYPE]: {
27
+ timeout: null,
28
+ events: [],
29
+ debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
30
+ eventName: PAGE_EVENT_TYPE,
31
+ },
32
+ [ERROR_EVENT_TYPE]: {
33
+ timeout: null,
34
+ events: [],
35
+ debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
36
+ eventName: ERROR_EVENT_TYPE,
37
+ },
38
+ [HTTP_EVENT_TYPE]: {
39
+ timeout: null,
40
+ events: [],
41
+ debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
42
+ eventName: HTTP_EVENT_TYPE,
43
+ },
44
+ [KEYBOARD_EVENT_TYPE]: {
45
+ timeout: null,
46
+ events: [],
47
+ debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
48
+ eventName: KEYBOARD_EVENT_TYPE,
49
+ },
50
+ };
51
+ this._setupUnloadHandler();
52
+ }
53
+ /**
54
+ * Creates an event object with the event and the time it was added then pushes
55
+ * that event object to the queue of events waiting to be debounced.
56
+ */
57
+ addEvent(event, type, occurredAt) {
58
+ if (!(type in this.eventsToDebounce)) {
59
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(new Error(`Type: ${type} is not in eventsToDebounce`), false, SEVERITY.error);
60
+ return;
61
+ }
62
+ if (!occurredAt) {
63
+ occurredAt = Date.now();
64
+ }
65
+ this.eventsToDebounce[type].events.push({
66
+ event,
67
+ occurredAt: new Date(timestampWrapper(occurredAt)).toISOString(),
68
+ });
69
+ this._debouncePvEvents(type);
70
+ }
71
+ /**
72
+ * Adds the events from the object to the page visit and sets up a timer
73
+ * to send the events if no more are received without the timeout
74
+ */
75
+ _debouncePvEvents(type) {
76
+ /**
77
+ * Debounce function to be executed once the debounce period is completed
78
+ */
79
+ const later = () => {
80
+ this.eventsToDebounce[type].timeout = null;
81
+ PageVisit.getInstance().addPageVisitEvents(this.eventsToDebounce[type].events, this.eventsToDebounce[type].eventName);
82
+ this.eventsToDebounce[type].events = [];
83
+ };
84
+ if (this.eventsToDebounce[type].timeout !== null) {
85
+ clearTimeout(this.eventsToDebounce[type].timeout);
86
+ }
87
+ this.eventsToDebounce[type].timeout = setTimeout(later, this.eventsToDebounce[type].debouncePeriod);
88
+ }
89
+ /** Sets up the page hide handler to try to push remaining events in the queues */
90
+ _setupUnloadHandler() {
91
+ addSafeEventListener(window, 'pagehide', () => {
92
+ Object.values(this.eventsToDebounce).forEach(eventObject => {
93
+ PageVisit.getInstance().addPageVisitEvents(eventObject.events, eventObject.eventName);
94
+ });
95
+ });
96
+ }
97
+ }
98
+
99
+ export { EventDebouncer };
@@ -1,8 +1,8 @@
1
1
  import { isValidURL, getJSStack, stringifyJSON, getMaxSubstringAllowed, asString } from '../utils/function.js';
2
2
  import { EVENT_ERROR_TYPE, URL_ATT_NAME, BLOCKLISTED_DOMAINS, ERROR_EVENT_TYPE, ERROR_EVENT_ERROR_TYPE, CUSTOM_ERROR_EVENT_TYPE, ERROR_EVENT_UNHANDLED_REJECTION_TYPE, ERROR_LOG_EVENT_ERROR_TYPE, FETCH_EXCEPTION_ERROR_TYPE, WRAPPED_EXCEPTION_ERROR_TYPE, GQL_ERROR_TYPE, RESPONSE_ERROR_TYPE, XML_HTTP_REQUEST_ERROR_TYPE, ERROR_SOURCE_ATT_NAME, TYPE_ATT_NAME, JS_EVENT_TYPE, JS_ERROR_ATT_NAME, JS_STACK_FRAMES_ATT_NAME, JS_STACK_FILE_ATT_NAME, JS_STACK_METHOD_ATT_NAME, SEVERITY, JS_STACK_MESSAGE_ATT_NAME, HTTP_EVENT_TYPE, NOIBU_INPUT_URLS, HTTP_CODE_ATT_NAME, PV_SEQ_ATT_NAME, GQL_EVENT_TYPE, GQL_ERROR_ATT_NAME } from '../constants.js';
3
3
  import ClientConfig from '../api/clientConfig.js';
4
- import { InputMonitor } from '../monitors/inputMonitor.js';
5
4
  import StoredMetrics from '../api/storedMetrics.js';
5
+ import { EventDebouncer } from './EventDebouncer.js';
6
6
 
7
7
  /** @module PageVisitEventError */
8
8
 
@@ -314,7 +314,7 @@ function saveErrorToPagevisit(type, payload, httpDataSeqNum) {
314
314
  // we register an error event
315
315
  StoredMetrics.getInstance().addError();
316
316
  // debounce event
317
- InputMonitor.getInstance().addEvent(pvError, ERROR_EVENT_TYPE);
317
+ EventDebouncer.getInstance().addEvent(pvError, ERROR_EVENT_TYPE);
318
318
  }
319
319
 
320
320
  export { getOnURL, isErrorCollectedByNoibu, saveErrorToPagevisit };