noibu-react-native 0.2.5 → 0.2.7

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 (98) hide show
  1. package/README.md +15 -15
  2. package/android/build.gradle +1 -1
  3. package/dist/api/{clientConfig.js → ClientConfig.js} +69 -52
  4. package/dist/api/{helpCode.js → HelpCode.js} +6 -13
  5. package/dist/api/InputManager.js +156 -0
  6. package/dist/api/{metroplexSocket.js → MetroplexSocket.js} +189 -178
  7. package/dist/api/StoredMetrics.js +158 -0
  8. package/dist/api/{storedPageVisit.js → StoredPageVisit.js} +61 -48
  9. package/dist/const_matchers.js +1 -5
  10. package/dist/constants.js +15 -390
  11. package/dist/entry/index.js +3 -4
  12. package/dist/entry/init.js +33 -19
  13. package/dist/monitors/AppNavigationMonitor.js +19 -19
  14. package/dist/monitors/BaseMonitor.js +9 -4
  15. package/dist/monitors/ClickMonitor.js +72 -76
  16. package/dist/monitors/ErrorMonitor.js +45 -55
  17. package/dist/monitors/KeyboardInputMonitor.js +13 -11
  18. package/dist/monitors/PageMonitor.js +25 -2
  19. package/dist/monitors/RequestMonitor.js +46 -57
  20. package/dist/monitors/http-tools/GqlErrorValidator.js +39 -69
  21. package/dist/monitors/http-tools/HTTPDataBundler.js +71 -66
  22. package/dist/monitors/integrations/{react-native-navigation-integration.js → ReactNativeNavigationIntegration.js} +15 -12
  23. package/dist/pageVisit/EventDebouncer.js +43 -74
  24. package/dist/pageVisit/HttpEventManager.js +88 -0
  25. package/dist/pageVisit/PageVisitManager.js +99 -0
  26. package/dist/pageVisit/pageVisitEventError.js +170 -280
  27. package/dist/react/ErrorBoundary.js +3 -6
  28. package/dist/sessionRecorder/{sessionRecorder.js → SessionRecorder.js} +58 -70
  29. package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -5
  30. package/dist/storage/{rnStorageProvider.js → RNStorageProvider.js} +3 -7
  31. package/dist/storage/{storage.js → Storage.js} +17 -30
  32. package/dist/storage/{storageProvider.js → StorageProvider.js} +7 -8
  33. package/dist/utils/date.js +39 -50
  34. package/dist/utils/eventlistener.js +5 -12
  35. package/dist/utils/function.js +42 -113
  36. package/dist/utils/log.js +5 -5
  37. package/dist/utils/object.js +12 -12
  38. package/dist/utils/piiRedactor.js +31 -3
  39. package/dist/utils/stacktrace-parser.js +29 -21
  40. package/package.json +14 -14
  41. package/dist/api/inputManager.js +0 -227
  42. package/dist/api/storedMetrics.js +0 -198
  43. package/dist/pageVisit/pageVisit.js +0 -181
  44. package/dist/pageVisit/pageVisitEventHTTP.js +0 -98
  45. package/dist/pageVisit/userStep.js +0 -20
  46. package/dist/src/api/clientConfig.d.ts +0 -100
  47. package/dist/src/api/clientConfig.test.d.ts +0 -1
  48. package/dist/src/api/helpCode.d.ts +0 -23
  49. package/dist/src/api/inputManager.d.ts +0 -87
  50. package/dist/src/api/metroplexSocket.d.ts +0 -137
  51. package/dist/src/api/storedMetrics.d.ts +0 -73
  52. package/dist/src/api/storedPageVisit.d.ts +0 -40
  53. package/dist/src/const_matchers.d.ts +0 -1
  54. package/dist/src/constants.d.ts +0 -290
  55. package/dist/src/entry/index.d.ts +0 -14
  56. package/dist/src/entry/init.d.ts +0 -5
  57. package/dist/src/monitors/AppNavigationMonitor.d.ts +0 -18
  58. package/dist/src/monitors/BaseMonitor.d.ts +0 -13
  59. package/dist/src/monitors/BaseMonitor.test.d.ts +0 -1
  60. package/dist/src/monitors/ClickMonitor.d.ts +0 -31
  61. package/dist/src/monitors/ErrorMonitor.d.ts +0 -63
  62. package/dist/src/monitors/KeyboardInputMonitor.d.ts +0 -20
  63. package/dist/src/monitors/PageMonitor.d.ts +0 -20
  64. package/dist/src/monitors/RequestMonitor.d.ts +0 -94
  65. package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +0 -59
  66. package/dist/src/monitors/http-tools/HTTPDataBundler.d.ts +0 -112
  67. package/dist/src/monitors/integrations/react-native-navigation-integration.d.ts +0 -20
  68. package/dist/src/pageVisit/EventDebouncer.d.ts +0 -24
  69. package/dist/src/pageVisit/pageVisit.d.ts +0 -52
  70. package/dist/src/pageVisit/pageVisitEventError.d.ts +0 -15
  71. package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +0 -25
  72. package/dist/src/pageVisit/userStep.d.ts +0 -5
  73. package/dist/src/react/ErrorBoundary.d.ts +0 -72
  74. package/dist/src/sessionRecorder/nativeSessionRecorderSubscription.d.ts +0 -79
  75. package/dist/src/sessionRecorder/sessionRecorder.d.ts +0 -60
  76. package/dist/src/sessionRecorder/types.d.ts +0 -91
  77. package/dist/src/storage/rnStorageProvider.d.ts +0 -23
  78. package/dist/src/storage/storage.d.ts +0 -39
  79. package/dist/src/storage/storageProvider.d.ts +0 -26
  80. package/dist/src/utils/date.d.ts +0 -6
  81. package/dist/src/utils/eventlistener.d.ts +0 -8
  82. package/dist/src/utils/function.d.ts +0 -102
  83. package/dist/src/utils/log.d.ts +0 -4
  84. package/dist/src/utils/object.d.ts +0 -44
  85. package/dist/src/utils/performance.d.ts +0 -6
  86. package/dist/src/utils/piiRedactor.d.ts +0 -11
  87. package/dist/src/utils/polyfills.d.ts +0 -4
  88. package/dist/src/utils/stacktrace-parser.d.ts +0 -7
  89. package/dist/types/Config.d.ts +0 -31
  90. package/dist/types/Metroplex.types.d.ts +0 -73
  91. package/dist/types/NavigationIntegration.d.ts +0 -6
  92. package/dist/types/PageVisit.types.d.ts +0 -8
  93. package/dist/types/PageVisitErrors.types.d.ts +0 -114
  94. package/dist/types/PageVisitEvents.types.d.ts +0 -91
  95. package/dist/types/PageVisitMetrics.types.d.ts +0 -27
  96. package/dist/types/Storage.d.ts +0 -14
  97. package/dist/types/StoredPageVisit.types.d.ts +0 -11
  98. package/dist/types/WrappedObjects.d.ts +0 -6
@@ -1,17 +1,45 @@
1
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';
2
+ import { SEVERITY, CONTENT_TYPE, PII_REDACTION_REPLACEMENT_STRING } from '../../constants.js';
3
+ import ClientConfig from '../../api/ClientConfig.js';
4
+ import StoredMetrics from '../../api/StoredMetrics.js';
5
5
  import { safeFromEntries, safeEntries } from '../../utils/object.js';
6
- import { safeTrim, stringifyJSON, isString } from '../../utils/function.js';
6
+ import { safeTrim, stringifyJSON } from '../../utils/function.js';
7
7
  import { removePII } from '../../utils/piiRedactor.js';
8
8
  import { Singleton } from '../BaseMonitor.js';
9
9
 
10
+ const DEFAULT_WEBSITE_SUBDOMAIN_PATTERN = /^www\d{0,2}$/;
11
+ const CONTENT_LENGTH = 'content-length';
12
+ const HTTP_BODY_DROPPED_TYPE_MSG = 'Dropped due to unsupported type.';
13
+ const HTTP_BODY_DROPPED_LENGTH_MSG = 'Dropped due to length.';
14
+ const HTTP_BODY_NULL_STRING = 'null';
15
+ // the maximum size of http data payload that will be capture for success, otherwise it is dropped
16
+ // this is 64k
17
+ const MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH = 65536;
18
+ // the maximum size of http data payload that will be captured, otherwise it is dropped
19
+ const MAX_HTTP_DATA_PAYLOAD_LENGTH = 50000;
20
+ // regex of human readable content based on content-type header
21
+ const HUMAN_READABLE_CONTENT_TYPE_REGEX = 'text|json|xml|html|graphql|x-www-form-urlencoded|form-data';
22
+ // HTTP request/response header keys to be blocked - these must be lowercase.
23
+ const BLOCKED_HTTP_HEADER_KEYS = [
24
+ 'authorization',
25
+ 'from',
26
+ 'proxy-authorization',
27
+ 'content-md5',
28
+ 'cookie',
29
+ 'x-forwarded-for',
30
+ 'x-real-ip',
31
+ 'x-device-id',
32
+ 'x-request-id',
33
+ 'x-auth-token',
34
+ 'x-user-id',
35
+ 'x-forwarded-for',
36
+ 'x-uidh',
37
+ 'set-cookie',
38
+ 'forwarded',
39
+ ];
10
40
  /** Bundles HTTP payloads and headers */
11
41
  class HTTPDataBundler extends Singleton {
12
- /**
13
- * Creates an instance of the ClickMonitor instance
14
- */
42
+ /** Creates an instance of the ClickMonitor instance */
15
43
  constructor() {
16
44
  super();
17
45
  // compile regex only once
@@ -32,11 +60,10 @@ class HTTPDataBundler extends Singleton {
32
60
  this.initialURLPartsReversed.reverse();
33
61
  }
34
62
  catch (e) {
35
- ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for initial URL: ${e}`, false, SEVERITY.warn);
63
+ ClientConfig.getInstance().postInternalError({ msg: `Unable to determine hostname for initial URL`, error: e }, false, SEVERITY.warn);
36
64
  }
37
65
  }
38
- this.httpDataCollectionEnabled =
39
- !!ClientConfig.getInstance().enableHttpDataCollection;
66
+ this.httpDataCollectionEnabled = !!ClientConfig.getInstance().enableHttpDataCollection;
40
67
  // compile the relative and full HTTP URL regexes
41
68
  const allowedURLs = ClientConfig.getInstance().listOfUrlsToCollectHttpDataFrom;
42
69
  this.httpDataAllowedAbsoluteRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, true);
@@ -47,7 +74,7 @@ class HTTPDataBundler extends Singleton {
47
74
  }
48
75
  /**
49
76
  * Builds the HTTP payload allowed regexes for full and relative URLs
50
- * @param allowedURLs A list of allowed URLs
77
+ * @param allowedURLs Target list of allowed URLs
51
78
  * @param absolute Use only absolute URLs if true, use only relative URL if false
52
79
  * @returns a regex of allowed URLs
53
80
  */
@@ -138,36 +165,34 @@ class HTTPDataBundler extends Singleton {
138
165
  return text;
139
166
  }
140
167
  catch (e) {
141
- ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify JSON response: ${e}`, false, SEVERITY.warn);
168
+ ClientConfig.getInstance().postInternalError({ msg: `Unable to stringify JSON response`, error: e }, false, SEVERITY.warn);
142
169
  return null;
143
170
  }
144
171
  }
145
172
  return null;
146
173
  });
147
174
  }
148
- /**
149
- * Builds an HTTP Data bundle
150
- */
151
- bundleHTTPData(url, requestHeaders, rawRequestPayload, responseHeaders, rawResponsePayload, method, isError) {
175
+ /** Builds an HTTP Data bundle */
176
+ bundleHTTPData(url, rawRequestHeaders, rawRequestPayload, rawResponseHeaders, rawResponsePayload, method, isError) {
152
177
  if (!this.isValidRequest(method)) {
153
178
  return null;
154
179
  }
155
180
  // stringify payload if correct type and not too large
156
- let requestPayload = '';
157
- let responsePayload = '';
181
+ let stringifiedRequestPayload = '';
182
+ let stringifiedResponsePayload = '';
158
183
  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);
184
+ stringifiedRequestPayload =
185
+ this.getReasonPayloadIsDropped(rawRequestHeaders, isError) ||
186
+ this.stringFromRequestBody(rawRequestPayload, rawRequestHeaders);
187
+ stringifiedResponsePayload =
188
+ this.getReasonPayloadIsDropped(rawResponseHeaders, isError) ||
189
+ this.stringFromRequestBody(rawResponsePayload, rawResponseHeaders);
165
190
  }
166
191
  // 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 || '';
192
+ const safeRequestHeaders = rawRequestHeaders || new Map();
193
+ const safeRequestPayload = stringifiedRequestPayload || '';
194
+ const safeResponseHeaders = rawResponseHeaders || new Map();
195
+ const safeResponsePayload = stringifiedResponsePayload || '';
171
196
  if (safeRequestHeaders.size === 0 &&
172
197
  !safeRequestPayload &&
173
198
  safeResponseHeaders.size === 0 &&
@@ -175,17 +200,17 @@ class HTTPDataBundler extends Singleton {
175
200
  return null;
176
201
  }
177
202
  // Ensure payloads do not exceed the maximum size and redact PII
178
- const requestPayloadUpdated = this.restrictPayload(safeRequestPayload, url, isError);
203
+ const requestPayload = this.restrictPayload(safeRequestPayload, url, isError);
179
204
  // Ensure payloads do not exceed the maximum size and redact PII
180
- const responsePayloadUpdated = this.restrictPayload(safeResponsePayload, url, isError);
205
+ const responsePayload = this.restrictPayload(safeResponsePayload, url, isError);
181
206
  // Redact PII.
182
- const cleanRequestHeaders = safeFromEntries(this.removePIIHeaders(requestHeaders));
183
- const cleanResponseHeaders = safeFromEntries(this.removePIIHeaders(responseHeaders));
207
+ const requestHeaders = safeFromEntries(this.removePIIHeaders(rawRequestHeaders));
208
+ const responseHeaders = safeFromEntries(this.removePIIHeaders(rawResponseHeaders));
184
209
  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,
210
+ rqh: requestHeaders,
211
+ rqp: requestPayload,
212
+ rsh: responseHeaders,
213
+ rsp: responsePayload,
189
214
  };
190
215
  }
191
216
  /**
@@ -193,7 +218,7 @@ class HTTPDataBundler extends Singleton {
193
218
  * de-duping the requests
194
219
  */
195
220
  isValidRequest(method) {
196
- return (this.httpDataCollectionEnabled && method && typeof method === 'string');
221
+ return this.httpDataCollectionEnabled && method && typeof method === 'string';
197
222
  }
198
223
  /**
199
224
  * Checks two things: that the URL is either on the same domain (or an address relative to the
@@ -256,7 +281,7 @@ class HTTPDataBundler extends Singleton {
256
281
  return HTTP_BODY_NULL_STRING;
257
282
  }
258
283
  if (typeof payload !== 'string') {
259
- ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient({
284
+ ClientConfig.getInstance().postInternalError({
260
285
  msg: `restrictPayload received non string payload`,
261
286
  payloadType: typeof payload,
262
287
  }, false, SEVERITY.error);
@@ -264,20 +289,12 @@ class HTTPDataBundler extends Singleton {
264
289
  }
265
290
  if (payload === HTTP_BODY_NULL_STRING ||
266
291
  (!!payload.startsWith &&
267
- (payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
268
- payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG))) ||
292
+ (payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) || payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG))) ||
269
293
  (!!payload.indexOf &&
270
- (payload.indexOf(HTTP_BODY_DROPPED_LENGTH_MSG) === 0 ||
271
- payload.indexOf(HTTP_BODY_DROPPED_TYPE_MSG) === 0))) {
294
+ (payload.indexOf(HTTP_BODY_DROPPED_LENGTH_MSG) === 0 || payload.indexOf(HTTP_BODY_DROPPED_TYPE_MSG) === 0))) {
272
295
  return payload;
273
296
  }
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
- }
297
+ const restrictSize = isError ? MAX_HTTP_DATA_PAYLOAD_LENGTH : MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
281
298
  if (payload.length > restrictSize) {
282
299
  StoredMetrics.getInstance().addHttpDataDropByLength();
283
300
  return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${payload.length}`;
@@ -290,19 +307,11 @@ class HTTPDataBundler extends Singleton {
290
307
  * Returns true if the content-length header is of acceptable size.
291
308
  * Too big gets rejected
292
309
  * If the headers are not found, check actual content for length
293
- * @param {Headers} headers
294
- * @returns boolean true if acceptable to collect
295
310
  */
296
311
  contentLengthAcceptable(headers, isError) {
297
312
  // TODO This entire check should move into the parent's parent so there is a single place
298
313
  // 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
- }
314
+ const restrictSize = isError ? MAX_HTTP_DATA_PAYLOAD_LENGTH : MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
306
315
  return this.contentLength(headers) <= restrictSize;
307
316
  }
308
317
  /**
@@ -314,8 +323,7 @@ class HTTPDataBundler extends Singleton {
314
323
  contentTypeAcceptable(headersMap) {
315
324
  // check content type
316
325
  const contentType = headersMap.get(CONTENT_TYPE);
317
- return !(contentType &&
318
- !this.contentTypeReadableRegex.test(contentType.toLowerCase()));
326
+ return !(contentType && !this.contentTypeReadableRegex.test(contentType.toLowerCase()));
319
327
  }
320
328
  /**
321
329
  * Returns a descriptive string if we have to drop payload based on the length
@@ -375,12 +383,9 @@ class HTTPDataBundler extends Singleton {
375
383
  if (value == null)
376
384
  return null;
377
385
  try {
378
- if (isString(value) && requestHeaders instanceof Map) {
386
+ if ((typeof value === 'string' || value instanceof String) && requestHeaders instanceof Map) {
379
387
  const contentType = requestHeaders.get(CONTENT_TYPE);
380
- if (contentType &&
381
- contentType
382
- .toLowerCase()
383
- .includes('application/x-www-form-urlencoded')) {
388
+ if (contentType && contentType.toLowerCase().includes('application/x-www-form-urlencoded')) {
384
389
  value = new URLSearchParams(value.toString());
385
390
  }
386
391
  }
@@ -423,7 +428,7 @@ class HTTPDataBundler extends Singleton {
423
428
  return stringifyJSON(value);
424
429
  }
425
430
  catch (e) {
426
- ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify request body: ${e}`, false, SEVERITY.warn);
431
+ ClientConfig.getInstance().postInternalError({ msg: `Unable to stringify request body`, error: e }, false, SEVERITY.warn);
427
432
  }
428
433
  return null;
429
434
  }
@@ -1,24 +1,22 @@
1
- /**
2
- * react-native-navigation adapter
3
- */
4
- class ReactNativeNavigationIntegration {
1
+ import { Singleton } from '../BaseMonitor.js';
2
+
3
+ /** react-native-navigation adapter */
4
+ class ReactNativeNavigationIntegration extends Singleton {
5
5
  constructor() {
6
+ super(...arguments);
6
7
  this.stack = [];
7
8
  this.stackPointers = {};
8
9
  }
9
- /**
10
- * attaches provided listeners to the integration
11
- */
10
+ /** Attaches provided listeners to the integration */
12
11
  register(navigation, onNavigation) {
13
- navigation
14
- .events()
15
- .registerComponentWillAppearListener(this.getListener(onNavigation));
12
+ if (this.registration) {
13
+ this.registration.remove();
14
+ }
15
+ this.registration = navigation.events().registerComponentWillAppearListener(this.getListener(onNavigation));
16
16
  }
17
17
  /**
18
18
  * Listens to ComponentWillAppear events, keeps track of visited screens and
19
19
  * pops them if the same page is visited to prevent cycles
20
- * @param onNavigation
21
- * @private
22
20
  */
23
21
  getListener(onNavigation) {
24
22
  return (event) => {
@@ -37,6 +35,11 @@ class ReactNativeNavigationIntegration {
37
35
  onNavigation(this.stack.slice());
38
36
  };
39
37
  }
38
+ /** Destructor */
39
+ destroy() {
40
+ console.log('child destroy');
41
+ this.registration.remove();
42
+ }
40
43
  }
41
44
 
42
45
  export { ReactNativeNavigationIntegration };
@@ -1,8 +1,7 @@
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';
1
+ import { PageVisitManager } from './PageVisitManager.js';
2
+ import { MAX_TIME_FOR_UNSENT_DATA_MILLIS } from '../constants.js';
3
+ import { getOccurredNow } from '../utils/date.js';
4
4
  import { addSafeEventListener } from '../utils/eventlistener.js';
5
- import ClientConfig from '../api/clientConfig.js';
6
5
  import { Singleton } from '../monitors/BaseMonitor.js';
7
6
 
8
7
  /** @module EventDebouncer */
@@ -11,42 +10,35 @@ import { Singleton } from '../monitors/BaseMonitor.js';
11
10
  * that are registered
12
11
  */
13
12
  class EventDebouncer extends Singleton {
14
- /**
15
- * Creates an instance of EventDebouncer
16
- */
13
+ /** Creates an instance of EventDebouncer */
17
14
  constructor() {
18
15
  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
- },
16
+ this.debouncePeriods = {
17
+ app_nav: 0,
18
+ page: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
19
+ err: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
20
+ http: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
21
+ userstep: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
22
+ };
23
+ this.timeouts = {
24
+ app_nav: null,
25
+ page: null,
26
+ err: null,
27
+ http: null,
28
+ userstep: null,
29
+ };
30
+ this.events = {
31
+ app_nav: [],
32
+ page: [],
33
+ err: [],
34
+ http: [],
35
+ userstep: [],
36
+ };
37
+ /** Debounce function to be executed once the debounce period is completed */
38
+ this.sendEvents = (type) => {
39
+ this.timeouts[type] = null;
40
+ PageVisitManager.getInstance().addPageVisitEvents(this.events[type]);
41
+ this.events[type] = [];
50
42
  };
51
43
  this._setupUnloadHandler();
52
44
  }
@@ -54,45 +46,22 @@ class EventDebouncer extends Singleton {
54
46
  * Creates an event object with the event and the time it was added then pushes
55
47
  * that event object to the queue of events waiting to be debounced.
56
48
  */
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);
49
+ debounce(event) {
50
+ const type = event.type;
51
+ const withTimestamp = Object.assign(Object.assign({}, event), { occ_at: getOccurredNow() });
52
+ const debouncePeriod = this.debouncePeriods[type];
53
+ this.events[type].push(withTimestamp);
54
+ clearTimeout(this.timeouts[type]);
55
+ this.timeouts[event.type] = setTimeout(() => this.sendEvents(type), debouncePeriod);
88
56
  }
89
57
  /** Sets up the page hide handler to try to push remaining events in the queues */
90
58
  _setupUnloadHandler() {
91
- addSafeEventListener(window, 'pagehide', () => {
92
- Object.values(this.eventsToDebounce).forEach(eventObject => {
93
- PageVisit.getInstance().addPageVisitEvents(eventObject.events, eventObject.eventName);
94
- });
95
- });
59
+ addSafeEventListener(window, 'pagehide', () => Object.keys(this.debouncePeriods).forEach((key) => this.sendEvents(key)));
60
+ }
61
+ destroy() {
62
+ Object.values(this.timeouts)
63
+ .filter(t => !!t)
64
+ .forEach(clearTimeout);
96
65
  }
97
66
  }
98
67
 
@@ -0,0 +1,88 @@
1
+ import { PageVisitManager } from './PageVisitManager.js';
2
+ import StoredMetrics from '../api/StoredMetrics.js';
3
+ import MetroplexSocket from '../api/MetroplexSocket.js';
4
+ import { safeTrim, getMaxSubstringAllowed, asString } from '../utils/function.js';
5
+ import { EventDebouncer } from './EventDebouncer.js';
6
+ import '../node_modules/@noibu/metroplex-ts-bindings/dist/index.js';
7
+ import { WebsocketMessageType } from '../node_modules/@noibu/metroplex-ts-bindings/dist/WebsocketMessageType.js';
8
+ import { EventType } from '../node_modules/@noibu/metroplex-ts-bindings/dist/EventType.js';
9
+
10
+ /** @module PageVisitEventHTTP */
11
+ /** http event manager */
12
+ // maximum number of HTTP data events including errors to collect per page visit
13
+ const MAX_HTTP_DATA_IF_ERROR_EVENT_COUNT = 120;
14
+ // maximum number of HTTP data events to collect per page visit
15
+ const MAX_HTTP_DATA_EVENT_COUNT = 100;
16
+ /** if no value or it's less than 0, fallback to 0 */
17
+ const validate = (value) => (!value || value < 0 ? 0 : value);
18
+ /** Saves the HTTP event to the pageVisit Queue */
19
+ function saveHTTPEvent(httpEvent, httpData, isGqlError = false) {
20
+ if (!httpEvent) {
21
+ return;
22
+ }
23
+ const url = safeTrim(getMaxSubstringAllowed(asString(httpEvent.url)));
24
+ // we do not store http events that have empty urls
25
+ if (!url) {
26
+ return;
27
+ }
28
+ const validatedEvent = {
29
+ code: validate(httpEvent.code),
30
+ r_time: validate(httpEvent.r_time),
31
+ mtd: (httpEvent.mtd || 'get').toUpperCase(),
32
+ url,
33
+ };
34
+ // we register an http event
35
+ StoredMetrics.getInstance().addHttpEvent();
36
+ // send http data down to metroplex
37
+ if (httpData) {
38
+ // add the sequence number to both events
39
+ const sequenceNumber = StoredMetrics.getInstance().httpSequenceNumber;
40
+ // restrict total number of events collected per page visit to ensure we don't
41
+ // blow up memory and storage usage
42
+ if (isSendAllowed(validatedEvent.code, sequenceNumber, isGqlError)) {
43
+ const httpDataWithSeq = Object.assign(Object.assign({}, httpData), { seq: sequenceNumber });
44
+ validatedEvent.seq = sequenceNumber;
45
+ // increment the count
46
+ StoredMetrics.getInstance().addHttpData();
47
+ MetroplexSocket.getInstance().sendMessage({ type: WebsocketMessageType.PageVisitHttp, payload: httpDataWithSeq });
48
+ }
49
+ else {
50
+ // have collected more than the max number of http requests for this
51
+ // page visit, so increment the over request limit count
52
+ StoredMetrics.getInstance().addHttpDataOverLimit();
53
+ }
54
+ }
55
+ const pve = {
56
+ type: EventType.Http,
57
+ http: validatedEvent,
58
+ };
59
+ // if this was an error, send immediately, so we don't lose it, delay otherwise
60
+ if (isAnError(validatedEvent.code, isGqlError)) {
61
+ PageVisitManager.getInstance().addPageVisitEvent(pve);
62
+ }
63
+ else {
64
+ EventDebouncer.getInstance().debounce(pve);
65
+ }
66
+ return validatedEvent.seq;
67
+ }
68
+ /** utility function to determine if status/flags combo is an error */
69
+ function isAnError(status, isGqlError) {
70
+ return isHttpCodeFailure(status) || isGqlError;
71
+ }
72
+ /** Determines if a response is a failure */
73
+ function isHttpCodeFailure(code) {
74
+ return typeof code !== 'number' ? true : code >= 400 || code <= 0;
75
+ }
76
+ /**
77
+ * Checks if sending data is allowed based on the HTTP status code and count.
78
+ * status - The HTTP status code to evaluate.
79
+ * count - The count of events to consider.
80
+ * isGqlError - Whether the context is considered as a GQL error.
81
+ * Returns `true` if sending data is allowed, `false` otherwise.
82
+ */
83
+ function isSendAllowed(status, count, isGqlError = false) {
84
+ const threshold = isAnError(status, isGqlError) ? MAX_HTTP_DATA_IF_ERROR_EVENT_COUNT : MAX_HTTP_DATA_EVENT_COUNT;
85
+ return count < threshold;
86
+ }
87
+
88
+ export { isHttpCodeFailure, isSendAllowed, saveHTTPEvent };
@@ -0,0 +1,99 @@
1
+ import ClientConfig from '../api/ClientConfig.js';
2
+ import MetroplexSocket from '../api/MetroplexSocket.js';
3
+ import StoredMetrics from '../api/StoredMetrics.js';
4
+ import { noibuLog } from '../utils/log.js';
5
+ import { Singleton } from '../monitors/BaseMonitor.js';
6
+ import { getOccurredNow } from '../utils/date.js';
7
+ import '../node_modules/@noibu/metroplex-ts-bindings/dist/index.js';
8
+ import { WebsocketMessageType } from '../node_modules/@noibu/metroplex-ts-bindings/dist/WebsocketMessageType.js';
9
+
10
+ /** @module Pagevisit */
11
+ const MAX_PAGEVISIT_EVENTS = 200;
12
+ const MAX_PAGEVISIT_PARTS = 10000;
13
+ /**
14
+ * Singleton class to hold all the information
15
+ * about the gathered errors throught the session
16
+ */
17
+ class PageVisitManager extends Singleton {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.partCounter = 0;
21
+ this.pvEvents = [];
22
+ this.pvEventLength = 0;
23
+ // variables used for monitoring our posting frequency
24
+ this.visibilityChangedCounter = 0;
25
+ this.totalPvEventLength = 0;
26
+ }
27
+ /** adds page visit events into the current page visit map and then sends a page visit message
28
+ */
29
+ addPageVisitEvents(eventObjects) {
30
+ eventObjects.forEach(eventObj => this._addPageVisitEvent(eventObj));
31
+ this._sendPageVisitMessage();
32
+ }
33
+ /**
34
+ * adds the page visit event into the current page visit map and then sends a page visit message
35
+ * returns the key to access this event in the map
36
+ */
37
+ addPageVisitEvent(eventObj) {
38
+ const withTimestamp = Object.assign(Object.assign({}, eventObj), { occ_at: getOccurredNow() });
39
+ noibuLog('addPageVisitEvent', withTimestamp);
40
+ this._addPageVisitEvent(withTimestamp);
41
+ this._sendPageVisitMessage();
42
+ }
43
+ /**
44
+ * adds a new page visit event into the current page visit map and returns the
45
+ * the key to access this event in the map
46
+ */
47
+ _addPageVisitEvent(pvEvent) {
48
+ // if we are over the limit set by the Beacon API limit, we need to
49
+ // send what we currently have in the buffer to metroplex
50
+ if (this.pvEventLength >= MAX_PAGEVISIT_EVENTS) {
51
+ this._sendPageVisitMessage();
52
+ }
53
+ // updating sizes
54
+ this.pvEvents.push(pvEvent);
55
+ this.pvEventLength += 1;
56
+ this.totalPvEventLength += 1;
57
+ }
58
+ /**
59
+ * _sendPageVisitMessage will reset the buffer and post the current
60
+ * content to metroplex
61
+ */
62
+ _sendPageVisitMessage() {
63
+ if (this.pvEvents.length === 0) {
64
+ /**
65
+ * don't send to metroplex if the event buffer is empty.
66
+ * Target previous call to this function from visibilityChange
67
+ * would have sent it as a final post. Another reason it would be empty is
68
+ * if the user has not done anything on the page and switched tabs,
69
+ * or closed the browser etc
70
+ */
71
+ return;
72
+ }
73
+ if (this.partCounter >= MAX_PAGEVISIT_PARTS) {
74
+ /**
75
+ * if we are attempting to send over the MAX_PAGEVISIT_PARTS
76
+ * number of parts then we block any subsequent part post to not
77
+ * overwhelm our back end. We lock the client for 10 minute, something
78
+ * must be going bad.
79
+ */
80
+ ClientConfig.getInstance().lockClientUntilNextPage({
81
+ msg: 'NoibuJS will stop processing parts because we reached max parts',
82
+ MAX_PAGEVISIT_PARTS,
83
+ totalPvEventLength: this.totalPvEventLength,
84
+ visibilityChangedCounter: this.visibilityChangedCounter,
85
+ });
86
+ return;
87
+ }
88
+ const pvp = MetroplexSocket.getInstance().addEndTimeToPayload({ events: this.pvEvents, pc: this.partCounter }, true);
89
+ StoredMetrics.getInstance().setPvPart(this.partCounter);
90
+ MetroplexSocket.getInstance().sendMessage({ payload: pvp, type: WebsocketMessageType.PageVisitPart });
91
+ // since we sent the content of the buffer to metroplex, we reset
92
+ // all variables that contained information about the past buffer.
93
+ this.pvEvents = [];
94
+ this.pvEventLength = 0;
95
+ this.partCounter += 1;
96
+ }
97
+ }
98
+
99
+ export { PageVisitManager };