noibu-react-native 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +155 -0
  2. package/dist/api/clientConfig.js +416 -0
  3. package/dist/api/helpCode.js +106 -0
  4. package/dist/api/inputManager.js +233 -0
  5. package/dist/api/metroplexSocket.js +882 -0
  6. package/dist/api/storedMetrics.js +201 -0
  7. package/dist/api/storedPageVisit.js +235 -0
  8. package/dist/const_matchers.js +260 -0
  9. package/dist/constants.d.ts +264 -0
  10. package/dist/constants.js +528 -0
  11. package/dist/entry/index.d.ts +8 -0
  12. package/dist/entry/index.js +15 -0
  13. package/dist/entry/init.js +91 -0
  14. package/dist/monitors/clickMonitor.js +284 -0
  15. package/dist/monitors/elementMonitor.js +174 -0
  16. package/dist/monitors/errorMonitor.js +295 -0
  17. package/dist/monitors/gqlErrorValidator.js +306 -0
  18. package/dist/monitors/httpDataBundler.js +665 -0
  19. package/dist/monitors/inputMonitor.js +130 -0
  20. package/dist/monitors/keyboardInputMonitor.js +67 -0
  21. package/dist/monitors/locationChangeMonitor.js +30 -0
  22. package/dist/monitors/pageMonitor.js +119 -0
  23. package/dist/monitors/requestMonitor.js +679 -0
  24. package/dist/pageVisit/pageVisit.js +172 -0
  25. package/dist/pageVisit/pageVisitEventError/pageVisitEventError.js +313 -0
  26. package/dist/pageVisit/pageVisitEventHTTP/pageVisitEventHTTP.js +115 -0
  27. package/dist/pageVisit/userStep/userStep.js +20 -0
  28. package/dist/react/ErrorBoundary.d.ts +72 -0
  29. package/dist/react/ErrorBoundary.js +102 -0
  30. package/dist/storage/localStorageProvider.js +23 -0
  31. package/dist/storage/rnStorageProvider.js +62 -0
  32. package/dist/storage/sessionStorageProvider.js +23 -0
  33. package/dist/storage/storage.js +119 -0
  34. package/dist/storage/storageProvider.js +83 -0
  35. package/dist/utils/date.js +62 -0
  36. package/dist/utils/eventlistener.js +67 -0
  37. package/dist/utils/function.js +398 -0
  38. package/dist/utils/object.js +144 -0
  39. package/dist/utils/performance.js +21 -0
  40. package/package.json +57 -0
@@ -0,0 +1,295 @@
1
+ import { replace } from '../utils/object.js';
2
+ import 'stacktrace-parser';
3
+ import 'react-native-device-info';
4
+ import { ERROR_EVENT_ERROR_TYPE, EVENT_TARGETS, CONSOLE_FUNCTION_OVERRIDES, WRAPPED_EXCEPTION_ERROR_TYPE, ERROR_LOG_EVENT_ERROR_TYPE } from '../constants.js';
5
+ import { saveErrorToPagevisit } from '../pageVisit/pageVisitEventError/pageVisitEventError.js';
6
+ import 'react-native-uuid';
7
+
8
+ /** @module ErrorMonitor */
9
+
10
+ /**
11
+ * will make sure that we ignore the next error caught by the error event listener
12
+ * ignoreError is used to not store errors that were generated by noibu wrapped
13
+ * code twice (once by wrapped code, once by error event listener).
14
+ */
15
+ function ignoreNextError() {
16
+
17
+ // this works because setTimeout without a delay fires
18
+ // as the immidiate next event in the event loop. Which should
19
+ // be right after the error that we throw before callign this error
20
+ setTimeout(() => {
21
+ });
22
+ }
23
+
24
+ /**
25
+ * wraps a provided function into a function that try and catches
26
+ * the original function, doing so allows us to catch errors
27
+ * functionToWrap: function to be wrapped
28
+ * @param {} functionToWrap
29
+ */
30
+ function wrap(functionToWrap) {
31
+ if (typeof functionToWrap !== 'function') {
32
+ return functionToWrap;
33
+ }
34
+
35
+ try {
36
+ // the function was wrapped already
37
+ if (functionToWrap.__noibu__) {
38
+ return functionToWrap;
39
+ }
40
+
41
+ // trying to wrap a function that was already wrapped in the past,
42
+ // return the wrapped value
43
+ if (functionToWrap.__noibu_wrapped__) {
44
+ return functionToWrap.__noibu_wrapped__;
45
+ }
46
+ } catch (err) {
47
+ // silently failling
48
+ return functionToWrap;
49
+ }
50
+ /**
51
+ * wrapper for most prototypes we know can generate errors
52
+ * @param {} ...args
53
+ */
54
+ const nbuWrapper = function (...args) {
55
+ // Trying to call the function in a wrapped try and catch
56
+ // this will generate an error in the console if there is one.
57
+ try {
58
+ // Recursively wrap arguments in case that some are
59
+ // functions
60
+ const wrappedArguments = Array.prototype.slice
61
+ .call(args)
62
+ .map(arg => wrap(arg));
63
+
64
+ // checking if the function is served through the handleEvent property
65
+ // for compatibility reasons
66
+ if (functionToWrap.handleEvent) {
67
+ return functionToWrap.handleEvent.apply(this, wrappedArguments);
68
+ }
69
+ return functionToWrap.apply(this, wrappedArguments);
70
+ // user land
71
+ } catch (exception) {
72
+ // we need to ignore this error during the onerror otherwise
73
+ // we catch it 2x
74
+ saveErrorToPagevisit(WRAPPED_EXCEPTION_ERROR_TYPE, {
75
+ error: exception,
76
+ });
77
+ ignoreNextError();
78
+ throw exception;
79
+ }
80
+ };
81
+
82
+ try {
83
+ // eslint-disable-next-line no-restricted-syntax
84
+ for (const property in functionToWrap) {
85
+ if (Object.prototype.hasOwnProperty.call(functionToWrap, property)) {
86
+ nbuWrapper[property] = functionToWrap[property];
87
+ }
88
+ }
89
+ } catch (err) {
90
+ // silently failling
91
+ }
92
+
93
+ nbuWrapper.prototype = functionToWrap.prototype || {};
94
+
95
+ Object.defineProperty(functionToWrap, '__noibu_wrapped__', {
96
+ enumerable: false,
97
+ value: nbuWrapper,
98
+ });
99
+
100
+ // Signal that this function has been wrapped/filled already
101
+ // for both debugging and to prevent it to being wrapped/filled twice
102
+ Object.defineProperties(nbuWrapper, {
103
+ __noibu__: {
104
+ enumerable: false,
105
+ value: true,
106
+ },
107
+ __noibu_original__: {
108
+ enumerable: false,
109
+ value: functionToWrap,
110
+ },
111
+ });
112
+
113
+ // Restore original function name (not all browsers allow that)
114
+ try {
115
+ const descriptor = Object.getOwnPropertyDescriptor(nbuWrapper, 'name');
116
+ if (descriptor.configurable) {
117
+ Object.defineProperty(nbuWrapper, 'name', {
118
+ // eslint-disable-next-line require-jsdoc
119
+ get() {
120
+ return functionToWrap.name;
121
+ },
122
+ });
123
+ }
124
+ } catch (err) {
125
+ return functionToWrap;
126
+ }
127
+
128
+ return nbuWrapper;
129
+ }
130
+
131
+ /**
132
+ * transform a log into an error since React hides component errors
133
+ * tps://reactjs.org/docs/error-boundaries.html
134
+ * @param {} errorLog
135
+ */
136
+ function processErrorLog(errorLog) {
137
+ // handle empty arguments
138
+ if (!errorLog) {
139
+ return;
140
+ }
141
+ const { message, stack } = errorLog;
142
+ // for now we only process error logs if they are a way to describe an error
143
+ if (!stack || !message) {
144
+ return;
145
+ }
146
+
147
+ saveErrorToPagevisit(ERROR_LOG_EVENT_ERROR_TYPE, { message, stack });
148
+ }
149
+
150
+ /** iterates arguments to try to extract errors from them
151
+ * @param {} argsFromErrorLog
152
+ */
153
+ function processErrorLogArguments(argsFromErrorLog) {
154
+ // we may receive multiple arguments
155
+ argsFromErrorLog.forEach(arg => {
156
+ // ensure arg exists before attempting processing
157
+ if (!arg) {
158
+ return;
159
+ }
160
+ // if we pass an array of elements in the console, we want to go through that
161
+ // away and make sure we are extracting the error object. We dont do nested arrays.
162
+ if (Array.isArray(arg)) {
163
+ arg.forEach(nestedArg => {
164
+ processErrorLog(nestedArg);
165
+ });
166
+ return;
167
+ }
168
+ processErrorLog(arg);
169
+ });
170
+ }
171
+
172
+ /**
173
+ * wraps and replaces the addEventListener property
174
+ * of event targets to try and catch errors
175
+ * eventTargetString: event target name
176
+ */
177
+ function configureEventListeners() {
178
+ EVENT_TARGETS.forEach(eventTargetString => {
179
+ const eventTarget = window[eventTargetString];
180
+ // The event target needs to have a prototype and have the
181
+ // addEventListener function
182
+ const proto = eventTarget && eventTarget.prototype;
183
+ if (
184
+ !proto ||
185
+ !proto.hasOwnProperty ||
186
+ // eslint-disable-next-line no-prototype-builtins
187
+ !proto.hasOwnProperty('addEventListener')
188
+ ) {
189
+ return;
190
+ }
191
+ replace(
192
+ proto,
193
+ 'addEventListener',
194
+ originalFunction =>
195
+ // We set nbuWrapper to the handler function so if this returns an error
196
+ // the trace message will start with nbuWrapper which would allow us to
197
+ // detect that this is a wrapped function and not a NoibuJS error
198
+ function nbuWrapper(eventName, handler, capture) {
199
+ // testPassive is done by passing a null handler, so we code
200
+ // against that
201
+ if (!handler) {
202
+ return originalFunction.call(this, eventName, handler, capture);
203
+ }
204
+ let wrappedHandler;
205
+ // Checking if handleEvent is explicitely
206
+ // passed for compatibility reason
207
+ if (handler.handleEvent) {
208
+ const wrappedHandleEvent = wrap(handler.handleEvent.bind(handler));
209
+ // in some case the handler is an object that contains much more than just the
210
+ // the single handle event property, in such cases we need to make sure we are not
211
+ // overwritting any other critical functionality by wrapping the event handler
212
+ wrappedHandler = handler;
213
+ wrappedHandler.handleEvent = wrappedHandleEvent;
214
+ } else {
215
+ wrappedHandler = wrap(handler);
216
+ }
217
+
218
+ return originalFunction.call(
219
+ this,
220
+ eventName,
221
+ wrappedHandler,
222
+ capture,
223
+ );
224
+ },
225
+ );
226
+
227
+ // replacing the removeEventListener function as well to wrap the listener
228
+ replace(
229
+ proto,
230
+ 'removeEventListener',
231
+ originalFunction =>
232
+ // We set nbuWrapper to the handler function so if this returns an error
233
+ // the trace message will start with nbuWrapper which would allow us to
234
+ // detect that this is a wrapped function and not a NoibuJS error
235
+ function nbuWrapper(eventName, handler, options) {
236
+ let callback = handler;
237
+ try {
238
+ callback = callback && (callback.__noibu_wrapped__ || callback);
239
+ } catch (e) {
240
+ // fail silently
241
+ }
242
+ return originalFunction.call(this, eventName, callback, options);
243
+ },
244
+ );
245
+ });
246
+
247
+ // we wrap all logging to catch any potential errors passed in logs
248
+ CONSOLE_FUNCTION_OVERRIDES.forEach(consoleFunction => {
249
+ if (!window.console || !window.console[consoleFunction]) {
250
+ return;
251
+ }
252
+ replace(
253
+ window.console,
254
+ consoleFunction,
255
+ originalFunction =>
256
+ // We set nbuWrapper to the handler function so if this returns an error
257
+ // the trace message will start with nbuWrapper which would allow us to
258
+ // detect that this is a wrapped function and not a NoibuJS error
259
+ function nbuWrapper() {
260
+ // ignoring this linting error since we don't want
261
+ // potentially overide the actual functioning of console.error
262
+ // eslint-disable-next-line prefer-rest-params
263
+ originalFunction.call(window.console, ...arguments);
264
+ // converting the arguments to an array so that we do not nest argument objects
265
+ // eslint-disable-next-line prefer-rest-params
266
+ processErrorLogArguments(Array.from(arguments));
267
+ },
268
+ );
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Monitors the errors happening on the window
274
+ */
275
+ function monitorErrors() {
276
+ // wrapping all listeners as soon as possible after we set the above listener
277
+ configureEventListeners();
278
+ global.ErrorUtils.setGlobalHandler((error, isFatal) => {
279
+ saveErrorToPagevisit(ERROR_EVENT_ERROR_TYPE, {
280
+ error,
281
+ });
282
+ }, true);
283
+ if (global.HermesInternal) {
284
+ global.HermesInternal.enablePromiseRejectionTracker({
285
+ allRejections: true,
286
+ onUnhandled: (id, rejection = {}) => {
287
+ saveErrorToPagevisit(ERROR_EVENT_ERROR_TYPE, {
288
+ error: rejection,
289
+ });
290
+ },
291
+ });
292
+ }
293
+ }
294
+
295
+ export { monitorErrors, processErrorLogArguments };
@@ -0,0 +1,306 @@
1
+ import { CONTENT_TYPE, SEVERITY_ERROR } from '../constants.js';
2
+ import { isInstanceOf, getMaxSubstringAllowed } from '../utils/function.js';
3
+ import ClientConfig from '../api/clientConfig.js';
4
+
5
+ const MESSAGE_ATT_NAME = 'message';
6
+ const EXTENSIONS_ATT_NAME = 'extensions';
7
+ const LOCATIONS_ATT_NAME = 'locations';
8
+ const PATH_ATT_NAME = 'path';
9
+ const LINE_ATT_NAME = 'line';
10
+ const COLUMN_ATT_NAME = 'column';
11
+ const MESSAGE_MAX_LENGTH = 1000;
12
+
13
+ /* eslint-disable no-restricted-syntax */
14
+ /* eslint-disable no-param-reassign */
15
+
16
+ /**
17
+ * Try detecting GraphQL errors from http response
18
+ */
19
+ class GqlErrorValidator {
20
+ /**
21
+ * Retrieves GQL error object based on fetch request/response
22
+ * @param {String} url
23
+ * @param {{}} options
24
+ * @param {Request} request
25
+ * @param {Response} response - cloned() from original response
26
+ */
27
+ static async fromFetch(url, options, request, response) {
28
+ try {
29
+ const isResponseValid = isInstanceOf(response, Response) && response.ok;
30
+ if (!isResponseValid) {
31
+ return null;
32
+ }
33
+ const contentType = this._getContentTypeFromFetchArguments(
34
+ options,
35
+ request,
36
+ );
37
+ if (this._shouldHandleRequest(url, contentType)) {
38
+ const data = await response.json();
39
+ return this._validate(data, []);
40
+ }
41
+ } catch (e) {
42
+ // can't read the response if request is aborted
43
+ // so just ignore it
44
+ if (!this._isRequestAborted(options, request)) {
45
+ this._postError(e);
46
+ }
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Retrieves GQL error object based on XHR object
54
+ * @param {String} url
55
+ * @param {XMLHttpRequest} xhr
56
+ */
57
+ static async fromXhr(url, xhr) {
58
+ try {
59
+ const isResponseValid =
60
+ isInstanceOf(xhr, XMLHttpRequest) &&
61
+ xhr.status >= 200 &&
62
+ xhr.status <= 299;
63
+ if (!isResponseValid) {
64
+ return null;
65
+ }
66
+ let contentType = null;
67
+ if (xhr.noibuRequestHeaders) {
68
+ contentType = xhr.noibuRequestHeaders.get(CONTENT_TYPE);
69
+ }
70
+ if (this._shouldHandleRequest(url, contentType)) {
71
+ let data = null;
72
+ if (xhr.responseType === 'blob') {
73
+ if (xhr.response.text) {
74
+ const content = await xhr.response.text();
75
+ data = this._parseJsonSafely(content);
76
+ }
77
+ } else if (xhr.responseType === 'json') {
78
+ data = xhr.response;
79
+ } else {
80
+ const content = xhr.responseText;
81
+ data = this._parseJsonSafely(content);
82
+ }
83
+ if (data) {
84
+ return this._validate(data, []);
85
+ }
86
+ }
87
+ } catch (e) {
88
+ this._postError(e);
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ /**
95
+ * Try safely parse a string and return null if fails
96
+ * @param {String} content
97
+ */
98
+ static _parseJsonSafely(content) {
99
+ try {
100
+ return JSON.parse(content);
101
+ } catch (e) {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Try to get content type for fetch arguments
108
+ * @param {{}} options
109
+ * @param {Request} request
110
+ */
111
+ static _getContentTypeFromFetchArguments(options, request) {
112
+ let headers = null;
113
+ if (isInstanceOf(request, Request)) {
114
+ headers = request.headers;
115
+ } else if (options && options.headers) {
116
+ headers = new Headers(options.headers);
117
+ }
118
+ let contentType = null;
119
+ if (headers) {
120
+ contentType = headers.get(CONTENT_TYPE);
121
+ }
122
+ return contentType;
123
+ }
124
+
125
+ /**
126
+ * Checks if request is aborted
127
+ * If it has been aborder we are not able to consume the response
128
+ * @param {{}} options
129
+ * @param {Request} request
130
+ */
131
+ static _isRequestAborted(options, request) {
132
+ if (isInstanceOf(request, Request)) {
133
+ return request.signal && request.signal.aborted;
134
+ }
135
+ if (options && isInstanceOf(options.signal, AbortSignal)) {
136
+ return options.signal.aborted;
137
+ }
138
+ return false;
139
+ }
140
+
141
+ /**
142
+ * Determines if request should be processed
143
+ * @param {String|URL} url
144
+ * @param {String} contentType
145
+ */
146
+ static _shouldHandleRequest(url, contentType) {
147
+ if (contentType) {
148
+ contentType = contentType.toLowerCase();
149
+ }
150
+ let isGqlUrl = false;
151
+ if (url) {
152
+ if (isInstanceOf(url, URL)) {
153
+ url = url.toString();
154
+ }
155
+ isGqlUrl = url.toLowerCase().includes('graphql');
156
+ }
157
+ return (
158
+ (contentType === 'application/json' && isGqlUrl) ||
159
+ contentType === 'application/graphql'
160
+ );
161
+ }
162
+
163
+ /**
164
+ * Sanitizes payload object
165
+ * @param {any} data
166
+ * @param {Array<String>} validationIssues
167
+ */
168
+ static _validate(data, validationIssues) {
169
+ let errors = null;
170
+
171
+ if (data && Array.isArray(data.errors)) {
172
+ errors = data.errors;
173
+
174
+ for (const error of errors) {
175
+ const properties = Object.keys(error);
176
+ for (const property of properties) {
177
+ switch (property) {
178
+ case MESSAGE_ATT_NAME:
179
+ this._validateMessage(error);
180
+ break;
181
+ case LOCATIONS_ATT_NAME:
182
+ this._validateLocations(error, validationIssues);
183
+ break;
184
+ case PATH_ATT_NAME:
185
+ this._validatePath(error, validationIssues);
186
+ break;
187
+ case EXTENSIONS_ATT_NAME:
188
+ this._validateExtensions(error);
189
+ break;
190
+ default:
191
+ delete error[property];
192
+ validationIssues.push(`unexpected error.${property}`);
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ if (validationIssues.length > 0) {
199
+ this._postValidationIssues(validationIssues);
200
+ }
201
+ }
202
+
203
+ return errors;
204
+ }
205
+
206
+ /**
207
+ * Sanitizes message object
208
+ * @param {any} error
209
+ */
210
+ static _validateMessage(error) {
211
+ error[MESSAGE_ATT_NAME] = getMaxSubstringAllowed(
212
+ error[MESSAGE_ATT_NAME],
213
+ MESSAGE_MAX_LENGTH,
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Sanitizes extensions object
219
+ * @param {any} error
220
+ * @param {Array<String>} validationIssues
221
+ */
222
+ static _validateExtensions(error) {
223
+ const json = JSON.stringify(error[EXTENSIONS_ATT_NAME]);
224
+ error[EXTENSIONS_ATT_NAME] = getMaxSubstringAllowed(
225
+ json,
226
+ MESSAGE_MAX_LENGTH,
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Sanitizes locations object
232
+ * @param {any} error
233
+ * @param {Array<String>} validationIssues
234
+ */
235
+ static _validateLocations(error, validationIssues) {
236
+ const locations = error[LOCATIONS_ATT_NAME];
237
+ if (Array.isArray(locations)) {
238
+ for (const location of locations) {
239
+ const properties = Object.keys(location);
240
+ for (const property of properties) {
241
+ switch (property) {
242
+ case LINE_ATT_NAME:
243
+ case COLUMN_ATT_NAME:
244
+ if (!Number.isSafeInteger(location[property])) {
245
+ const value = location[property];
246
+ location[property] = 0;
247
+ validationIssues.push(
248
+ `unexpected ${property} value '${value}'`,
249
+ );
250
+ }
251
+ break;
252
+ default:
253
+ delete location[property];
254
+ validationIssues.push(`unexpected error.location.${property}`);
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ } else {
260
+ delete error[LOCATIONS_ATT_NAME];
261
+ validationIssues.push(`unexpected error.locations`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Sanitizes path object
267
+ * @param {any} error
268
+ * @param {Array<String>} validationIssues
269
+ */
270
+ static _validatePath(error, validationIssues) {
271
+ const path = error[PATH_ATT_NAME];
272
+ if (Array.isArray(path)) {
273
+ error[PATH_ATT_NAME] = error[PATH_ATT_NAME].map(x => x.toString());
274
+ } else {
275
+ delete error[PATH_ATT_NAME];
276
+ validationIssues.push(`unexpected error.path`);
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Posts error
282
+ * @param {String} message
283
+ */
284
+ static _postError(message) {
285
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
286
+ `GQL parse error: ${message}`,
287
+ false,
288
+ SEVERITY_ERROR,
289
+ );
290
+ }
291
+
292
+ /**
293
+ * Posts issue found during object sanitization
294
+ * @param {Array<String>} validationIssues
295
+ */
296
+ static _postValidationIssues(validationIssues) {
297
+ const message = validationIssues.join(',');
298
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
299
+ `GQL error validation warning: ${message}`,
300
+ false,
301
+ SEVERITY_ERROR,
302
+ );
303
+ }
304
+ }
305
+
306
+ export { GqlErrorValidator as default };