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.
- package/README.md +1 -1
- package/dist/api/clientConfig.js +225 -217
- package/dist/api/helpCode.js +61 -87
- package/dist/api/metroplexSocket.js +460 -463
- package/dist/api/storedPageVisit.js +150 -208
- package/dist/constants.js +10 -2
- package/dist/entry/init.js +65 -63
- package/dist/monitors/{appNavigationMonitor.js → AppNavigationMonitor.js} +12 -22
- package/dist/monitors/ClickMonitor.js +198 -0
- package/dist/monitors/ErrorMonitor.js +206 -0
- package/dist/monitors/KeyboardInputMonitor.js +60 -0
- package/dist/monitors/PageMonitor.js +98 -0
- package/dist/monitors/RequestMonitor.js +390 -0
- package/dist/monitors/http-tools/GqlErrorValidator.js +259 -0
- package/dist/monitors/http-tools/HTTPDataBundler.js +458 -0
- package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
- package/dist/pageVisit/EventDebouncer.js +99 -0
- package/dist/pageVisit/pageVisitEventError.js +2 -2
- package/dist/pageVisit/pageVisitEventHTTP.js +79 -93
- package/dist/react/ErrorBoundary.js +18 -15
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
- package/dist/sessionRecorder/sessionRecorder.js +152 -151
- package/dist/{api → src/api}/clientConfig.d.ts +2 -2
- package/dist/{api → src/api}/helpCode.d.ts +10 -16
- package/dist/{api → src/api}/metroplexSocket.d.ts +48 -67
- package/dist/{api → src/api}/storedPageVisit.d.ts +12 -21
- package/dist/{constants.d.ts → src/constants.d.ts} +45 -0
- package/dist/{entry → src/entry}/init.d.ts +1 -1
- package/dist/src/monitors/AppNavigationMonitor.d.ts +18 -0
- package/dist/src/monitors/ClickMonitor.d.ts +31 -0
- package/dist/src/monitors/ErrorMonitor.d.ts +63 -0
- package/dist/{monitors/keyboardInputMonitor.d.ts → src/monitors/KeyboardInputMonitor.d.ts} +7 -4
- package/dist/{monitors/pageMonitor.d.ts → src/monitors/PageMonitor.d.ts} +6 -8
- package/dist/src/monitors/RequestMonitor.d.ts +94 -0
- package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +59 -0
- package/dist/src/monitors/http-tools/HTTPDataBundler.d.ts +112 -0
- package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +3 -2
- package/dist/src/pageVisit/EventDebouncer.d.ts +24 -0
- package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +1 -1
- package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
- package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
- package/dist/{storage → src/storage}/rnStorageProvider.d.ts +1 -1
- package/dist/{storage → src/storage}/storage.d.ts +2 -2
- package/dist/{storage → src/storage}/storageProvider.d.ts +3 -3
- package/dist/{utils → src/utils}/function.d.ts +27 -7
- package/dist/{utils → src/utils}/object.d.ts +11 -8
- package/dist/src/utils/piiRedactor.d.ts +11 -0
- package/dist/src/utils/polyfills.d.ts +4 -0
- package/dist/storage/rnStorageProvider.js +7 -4
- package/dist/storage/storage.js +43 -35
- package/dist/storage/storageProvider.js +23 -19
- package/dist/types/Config.d.ts +24 -20
- package/dist/types/Metroplex.types.d.ts +73 -0
- package/dist/types/Monitor.d.ts +11 -0
- package/dist/types/Monitor.js +19 -0
- package/dist/types/PageVisit.types.d.ts +8 -0
- package/dist/types/PageVisitErrors.types.d.ts +114 -0
- package/dist/types/PageVisitEvents.types.d.ts +91 -0
- package/dist/types/PageVisitMetrics.types.d.ts +27 -0
- package/dist/types/Storage.d.ts +1 -1
- package/dist/types/StoredPageVisit.types.d.ts +4 -47
- package/dist/types/WrappedObjects.d.ts +6 -0
- package/dist/utils/function.js +110 -77
- package/dist/utils/object.js +59 -6
- package/dist/utils/piiRedactor.js +98 -0
- package/dist/utils/polyfills.js +24 -0
- package/package.json +8 -8
- package/dist/monitors/appNavigationMonitor.d.ts +0 -22
- package/dist/monitors/clickMonitor.d.ts +0 -44
- package/dist/monitors/clickMonitor.js +0 -251
- package/dist/monitors/errorMonitor.d.ts +0 -28
- package/dist/monitors/errorMonitor.js +0 -180
- package/dist/monitors/gqlErrorValidator.d.ts +0 -82
- package/dist/monitors/gqlErrorValidator.js +0 -306
- package/dist/monitors/httpDataBundler.d.ts +0 -161
- package/dist/monitors/httpDataBundler.js +0 -725
- package/dist/monitors/inputMonitor.d.ts +0 -34
- package/dist/monitors/inputMonitor.js +0 -138
- package/dist/monitors/keyboardInputMonitor.js +0 -66
- package/dist/monitors/pageMonitor.js +0 -122
- package/dist/monitors/requestMonitor.d.ts +0 -10
- package/dist/monitors/requestMonitor.js +0 -401
- package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
- package/dist/types/PageVisit.d.ts +0 -22
- package/dist/types/ReactNative.d.ts +0 -4
- package/dist/types/globals.d.ts +0 -45
- /package/dist/{api → src/api}/inputManager.d.ts +0 -0
- /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
- /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
- /package/dist/{entry → src/entry}/index.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
- /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
- /package/dist/{utils → src/utils}/date.d.ts +0 -0
- /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
- /package/dist/{utils → src/utils}/log.d.ts +0 -0
- /package/dist/{utils → src/utils}/performance.d.ts +0 -0
- /package/dist/{utils → src/utils}/stacktrace-parser.d.ts +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import Pressability from 'react-native/Libraries/Pressability/Pressability';
|
|
2
|
+
import { WHITELIST_HTML_ID_TEXT_REGEX, USERSTEP_EVENT_TYPE, SOURCE_ATT_NAME, TEXT_ATT_NAME, TAGNAME_ATT_NAME, HTMLID_ATT_NAME, TYPE_ATT_NAME, CLICK_EVENT_TYPE, CSS_CLASS_ATT_NAME } from '../constants.js';
|
|
3
|
+
import { PageVisit } from '../pageVisit/pageVisit.js';
|
|
4
|
+
import { updatePayload } from '../pageVisit/userStep.js';
|
|
5
|
+
import StoredMetrics from '../api/storedMetrics.js';
|
|
6
|
+
import { WHITELIST_TEXT_REGEX_STRING } from '../const_matchers.js';
|
|
7
|
+
import { maskTextInput } from '../utils/function.js';
|
|
8
|
+
import { timestampWrapper } from '../utils/date.js';
|
|
9
|
+
import ClientConfig from '../api/clientConfig.js';
|
|
10
|
+
import { Singleton } from '../types/Monitor.js';
|
|
11
|
+
|
|
12
|
+
/** @module ClickMonitor */
|
|
13
|
+
/** Monitors the clicks which we capture and later process */
|
|
14
|
+
class ClickMonitor extends Singleton {
|
|
15
|
+
/** Starts monitoring clicks on every Press-able component */
|
|
16
|
+
monitor() {
|
|
17
|
+
if (!ClickMonitor.originalCreateEventHandlers) {
|
|
18
|
+
ClickMonitor.originalCreateEventHandlers =
|
|
19
|
+
Pressability.prototype.getEventHandlers;
|
|
20
|
+
/**
|
|
21
|
+
* Proxies prototype method
|
|
22
|
+
*/
|
|
23
|
+
Pressability.prototype.getEventHandlers = function () {
|
|
24
|
+
const ehs = ClickMonitor.originalCreateEventHandlers.call(this);
|
|
25
|
+
return Object.fromEntries(Object.entries(ehs).map(([key, handler]) => [
|
|
26
|
+
key,
|
|
27
|
+
(event) => {
|
|
28
|
+
if (key === 'onResponderRelease') {
|
|
29
|
+
ClickMonitor.onClickHandle(event);
|
|
30
|
+
}
|
|
31
|
+
return handler(event);
|
|
32
|
+
},
|
|
33
|
+
]));
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Handles a single click event
|
|
39
|
+
*/
|
|
40
|
+
static onClickHandle(event) {
|
|
41
|
+
if (event) {
|
|
42
|
+
const { _targetInst: target } = event;
|
|
43
|
+
const targetClassName = target.elementType;
|
|
44
|
+
// if the tag name of the src element is image, then we need
|
|
45
|
+
// to process the image name, else we need to get the textual content
|
|
46
|
+
// todo process images
|
|
47
|
+
const text = ClickMonitor.getTextualContentFromEl(target);
|
|
48
|
+
let textFromElement = ClickMonitor.trimText(text);
|
|
49
|
+
let tagName = '';
|
|
50
|
+
if (typeof targetClassName === 'string') {
|
|
51
|
+
tagName = targetClassName.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
// id of element
|
|
54
|
+
let hid = target.memoizedProps.testID || '';
|
|
55
|
+
// in some bizarre cases, the html id of an element gets overriden
|
|
56
|
+
// to contain jquery objects. If the hid is an object, it's of no
|
|
57
|
+
// use to us.
|
|
58
|
+
if (typeof hid !== 'string') {
|
|
59
|
+
hid = '';
|
|
60
|
+
}
|
|
61
|
+
// if we find that the text matches analytic data used
|
|
62
|
+
// to find checkout starts, add to cart clicks, etc.
|
|
63
|
+
// we do not mask it.
|
|
64
|
+
if (!ClickMonitor.textCapturedWhiteListRegex.test(textFromElement) &&
|
|
65
|
+
!ClickMonitor.htmlIDAllowListRegex.test(hid)) {
|
|
66
|
+
if (tagName === 'input') {
|
|
67
|
+
if (event.type &&
|
|
68
|
+
(event.type === 'button' || event.type === 'submit')) ;
|
|
69
|
+
else {
|
|
70
|
+
textFromElement = '*';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (tagName === 'textarea') {
|
|
74
|
+
textFromElement = '*';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
textFromElement = maskTextInput(textFromElement);
|
|
78
|
+
const tPayload = {
|
|
79
|
+
[SOURCE_ATT_NAME]: '',
|
|
80
|
+
[TEXT_ATT_NAME]: textFromElement,
|
|
81
|
+
[TAGNAME_ATT_NAME]: tagName,
|
|
82
|
+
[HTMLID_ATT_NAME]: hid,
|
|
83
|
+
[TYPE_ATT_NAME]: CLICK_EVENT_TYPE,
|
|
84
|
+
[CSS_CLASS_ATT_NAME]: '',
|
|
85
|
+
};
|
|
86
|
+
StoredMetrics.getInstance().addPvClick();
|
|
87
|
+
PageVisit.getInstance().addPageVisitEvents([
|
|
88
|
+
{
|
|
89
|
+
event: updatePayload(tPayload),
|
|
90
|
+
occurredAt: new Date(timestampWrapper(Date.now())).toISOString(),
|
|
91
|
+
},
|
|
92
|
+
], USERSTEP_EVENT_TYPE);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Gets the textual content from an element, if any
|
|
96
|
+
*/
|
|
97
|
+
static getTextualContentFromEl(element) {
|
|
98
|
+
return ClickMonitor.parseInnerContent(element, '', 100, {
|
|
99
|
+
value: 0,
|
|
100
|
+
limit: 100,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Parse and trim text */
|
|
104
|
+
static trimText(text) {
|
|
105
|
+
// regex to replace all whitespace with a single space and trim text
|
|
106
|
+
let parsedText = text.trim().replace(/\s+/g, ' ');
|
|
107
|
+
// if there is text that is longer than average sentence length,
|
|
108
|
+
// chances are that it is not a valid text so we need to further process it
|
|
109
|
+
if (parsedText.length > 100) {
|
|
110
|
+
const index = parsedText.lastIndexOf(' ', 97);
|
|
111
|
+
// We can not chop the text at exactly 97 characters since chopping in the
|
|
112
|
+
// middle of an encoded value may cause deserialization issues. Chop at the
|
|
113
|
+
// last ' ' character before the 97th character.
|
|
114
|
+
if (index > 0) {
|
|
115
|
+
parsedText = `${parsedText.substring(0, index)}...`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// If there are no ' ' characters then the text is likely not valid so just
|
|
119
|
+
// return '...'.
|
|
120
|
+
parsedText = '...';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return parsedText;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Recursively parses element's inner content and masks blocked css classes
|
|
127
|
+
*/
|
|
128
|
+
static parseInnerContent(element, text, textLimit, counter) {
|
|
129
|
+
/* eslint-disable no-restricted-syntax */
|
|
130
|
+
/* eslint-disable no-param-reassign */
|
|
131
|
+
if (text.length >= textLimit) {
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
if (counter.value >= counter.limit) {
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
counter.value += 1;
|
|
138
|
+
if (ClickMonitor.getBlockedElements().includes(element.memoizedProps.testID)) {
|
|
139
|
+
return `${text}${text ? ' ' : ''}*`;
|
|
140
|
+
}
|
|
141
|
+
// eslint-disable-next-line require-jsdoc
|
|
142
|
+
const walk = (node) => {
|
|
143
|
+
var _a;
|
|
144
|
+
// Check if the node is a Text element
|
|
145
|
+
if (typeof node.elementType === 'object' &&
|
|
146
|
+
((_a = node.elementType) === null || _a === void 0 ? void 0 : _a.displayName) === 'Text' &&
|
|
147
|
+
node.memoizedProps &&
|
|
148
|
+
typeof node.memoizedProps.children === 'string') {
|
|
149
|
+
text = ClickMonitor.parseAndAppendText(text, [
|
|
150
|
+
node.memoizedProps.children,
|
|
151
|
+
]);
|
|
152
|
+
if (text.length >= textLimit)
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// If the node has children, traverse them
|
|
156
|
+
if (node.child)
|
|
157
|
+
walk(node.child);
|
|
158
|
+
// After traversing children, traverse siblings
|
|
159
|
+
if (node.sibling)
|
|
160
|
+
walk(node.sibling);
|
|
161
|
+
};
|
|
162
|
+
walk(element);
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Gets selectors to prevent those elements from being recorded
|
|
167
|
+
*/
|
|
168
|
+
static getBlockedElements() {
|
|
169
|
+
const selectors = ClientConfig.getInstance().blockedElements;
|
|
170
|
+
const blockedElements = ['noibu-blocked'];
|
|
171
|
+
if (selectors && Array.isArray(selectors)) {
|
|
172
|
+
blockedElements.push(...selectors);
|
|
173
|
+
}
|
|
174
|
+
return blockedElements;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* normalize value and append to the resulting text if not empty
|
|
178
|
+
*/
|
|
179
|
+
static parseAndAppendText(text, values) {
|
|
180
|
+
const goodValues = [];
|
|
181
|
+
for (const v of values) {
|
|
182
|
+
if (Number.isFinite(v) || typeof v === 'string') {
|
|
183
|
+
goodValues.push(v);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (let value of goodValues) {
|
|
187
|
+
value = `${value}`.trim().replace(/\s+/g, ' ');
|
|
188
|
+
if (value.length > 0) {
|
|
189
|
+
return text + (text ? ' ' : '') + value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
ClickMonitor.textCapturedWhiteListRegex = new RegExp(WHITELIST_TEXT_REGEX_STRING(), 'i');
|
|
196
|
+
ClickMonitor.htmlIDAllowListRegex = new RegExp(WHITELIST_HTML_ID_TEXT_REGEX, 'i');
|
|
197
|
+
|
|
198
|
+
export { ClickMonitor };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { asString, isStackTrace } from '../utils/function.js';
|
|
2
|
+
import { ERROR_EVENT_ERROR_TYPE, STACK_TRACE_SANITIZE_REGEXP, ERROR_EVENT_UNHANDLED_REJECTION_TYPE, CONSOLE_FUNCTION_OVERRIDES, ERROR_LOG_EVENT_ERROR_TYPE } from '../constants.js';
|
|
3
|
+
import { saveErrorToPagevisit } from '../pageVisit/pageVisitEventError.js';
|
|
4
|
+
import { replace } from '../utils/object.js';
|
|
5
|
+
import { Singleton } from '../types/Monitor.js';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/ban-types,prefer-arrow-callback */
|
|
8
|
+
/** @module ErrorMonitor */
|
|
9
|
+
let ignoreError = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Monitors the errors happening on the window
|
|
12
|
+
*/
|
|
13
|
+
class ErrorMonitor extends Singleton {
|
|
14
|
+
/** base method */
|
|
15
|
+
monitor() {
|
|
16
|
+
ErrorMonitor.configureEventListeners();
|
|
17
|
+
ErrorMonitor.configureHermesHooks();
|
|
18
|
+
ErrorMonitor.configureErrorUtilsHandler();
|
|
19
|
+
}
|
|
20
|
+
/** proxy error utils */
|
|
21
|
+
static configureErrorUtilsHandler() {
|
|
22
|
+
if (typeof ErrorUtils === 'undefined') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const existingHandler = ErrorUtils.getGlobalHandler();
|
|
26
|
+
ErrorUtils.setGlobalHandler((error, ...rest) => {
|
|
27
|
+
ErrorMonitor.onErrorHandler(error);
|
|
28
|
+
if (typeof existingHandler === 'function') {
|
|
29
|
+
return existingHandler(error, ...rest);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Handles a single error event
|
|
35
|
+
*/
|
|
36
|
+
static onErrorHandler(error) {
|
|
37
|
+
if (!error || ErrorMonitor.shouldIgnoreError()) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
saveErrorToPagevisit(ERROR_EVENT_ERROR_TYPE, {
|
|
41
|
+
error,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** proxy hermes engine */
|
|
45
|
+
static configureHermesHooks() {
|
|
46
|
+
var _a;
|
|
47
|
+
if (typeof HermesInternal !== 'undefined' && HermesInternal !== null) {
|
|
48
|
+
(_a = HermesInternal.enablePromiseRejectionTracker) === null || _a === void 0 ? void 0 : _a.call(HermesInternal, {
|
|
49
|
+
allRejections: true,
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* This internal promise implementation method is populated only after enabling the promise tracker.
|
|
53
|
+
* It represents an improvement over the previous approach,
|
|
54
|
+
* which would lose stack context regarding the rejection because it ran asynchronously through setTimeout.
|
|
55
|
+
*
|
|
56
|
+
* This updated method ensures synchronous error capturing and retrieves the correct stack frames.
|
|
57
|
+
*/
|
|
58
|
+
replace(Promise, '_m', (originalFunction) => function nbuGlobalPromiseRejectWrapper(promise, error) {
|
|
59
|
+
if (error.message && error.stack) {
|
|
60
|
+
ErrorMonitor.onPromiseRejectionHandler(error);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
ErrorMonitor.onPromiseRejectionHandler(new Error(asString(error)));
|
|
64
|
+
}
|
|
65
|
+
return originalFunction === null || originalFunction === void 0 ? void 0 : originalFunction(promise, error);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* handler for promise rejection failures
|
|
71
|
+
*/
|
|
72
|
+
static onPromiseRejectionHandler(error) {
|
|
73
|
+
if (!error || !error.message || !error.stack) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const sanitizedStack = error.stack
|
|
77
|
+
.split('\n')
|
|
78
|
+
.filter(line => !line.match(STACK_TRACE_SANITIZE_REGEXP))
|
|
79
|
+
.join('\n');
|
|
80
|
+
const payload = {
|
|
81
|
+
error: {
|
|
82
|
+
message: error.message,
|
|
83
|
+
stack: sanitizedStack,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
saveErrorToPagevisit(ERROR_EVENT_UNHANDLED_REJECTION_TYPE, payload);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* wraps and replaces the addEventListener property
|
|
90
|
+
* of event targets to try and catch errors
|
|
91
|
+
* eventTargetString: event target name
|
|
92
|
+
*/
|
|
93
|
+
static configureEventListeners() {
|
|
94
|
+
// we wrap all logging to catch any potential errors passed in logs
|
|
95
|
+
CONSOLE_FUNCTION_OVERRIDES.forEach(consoleFunction => {
|
|
96
|
+
if (!window.console || !window.console[consoleFunction]) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
replace(window.console, consoleFunction, function (originalFunction) {
|
|
100
|
+
// We set nbuWrapper to the handler function so if this returns an error
|
|
101
|
+
// the trace message will start with nbuWrapper which would allow us to
|
|
102
|
+
// detect that this is a wrapped function and not a NoibuJS error
|
|
103
|
+
return function nbuWrapper() {
|
|
104
|
+
// ignoring this linting error since we don't want
|
|
105
|
+
// potentially overide the actual functioning of console.error
|
|
106
|
+
// eslint-disable-next-line prefer-rest-params
|
|
107
|
+
originalFunction.call(window.console, ...arguments);
|
|
108
|
+
// converting the arguments to an array so that we do not nest argument objects
|
|
109
|
+
// eslint-disable-next-line prefer-rest-params
|
|
110
|
+
ErrorMonitor.processErrorLogArguments(Array.from(arguments));
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** iterates arguments to try to extract errors from them
|
|
116
|
+
*/
|
|
117
|
+
static processErrorLogArguments(argsFromErrorLog) {
|
|
118
|
+
// tracks if arguments were processed at least once successfully, and if not, tries to reconstruct an Error
|
|
119
|
+
const collectedArgs = [];
|
|
120
|
+
// we may receive multiple arguments
|
|
121
|
+
argsFromErrorLog.forEach(arg => {
|
|
122
|
+
// ensure arg exists before attempting processing
|
|
123
|
+
if (!arg) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// if we pass an array of elements in the console, we want to go through that
|
|
127
|
+
// away and make sure we are extracting the error object. We don't do nested arrays.
|
|
128
|
+
if (Array.isArray(arg)) {
|
|
129
|
+
collectedArgs.push(...arg);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
collectedArgs.push(arg);
|
|
133
|
+
});
|
|
134
|
+
const maybeErrors = collectedArgs.filter(arg => !ErrorMonitor.processErrorLog(arg));
|
|
135
|
+
// if previous process calls return nothing,
|
|
136
|
+
// try to reconstruct error from the args
|
|
137
|
+
ErrorMonitor.constructErrors(maybeErrors).forEach(ErrorMonitor.processErrorLog);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Constructs error objects based on console args.
|
|
141
|
+
* Relies on isStackTrace() to determine if arg is a stack trace, otherwise the arg must be a message.
|
|
142
|
+
*
|
|
143
|
+
* If there are multiple stack traces or the number of error messages is not equal to one,
|
|
144
|
+
* it maps the stack traces to objects with their respective first lines as messages.
|
|
145
|
+
* Otherwise, if there is exactly one stack trace and one error message,
|
|
146
|
+
* it constructs an array with a single object containing the first stack trace and the first error message.
|
|
147
|
+
* @param {Array<string>} args
|
|
148
|
+
*/
|
|
149
|
+
static constructErrors(args) {
|
|
150
|
+
if (args.length === 0) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
if (args.length === 2) {
|
|
154
|
+
if (isStackTrace(args[0])) {
|
|
155
|
+
return [{ stack: args[0], message: args[1] }];
|
|
156
|
+
}
|
|
157
|
+
if (isStackTrace(args[1])) {
|
|
158
|
+
return [{ stack: args[1], message: args[0] }];
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const stacks = [];
|
|
163
|
+
const messages = [];
|
|
164
|
+
args.forEach(sm => {
|
|
165
|
+
if (isStackTrace()) {
|
|
166
|
+
stacks.push(sm);
|
|
167
|
+
const lines = sm.split('\n');
|
|
168
|
+
if (isStackTrace(lines[0])) {
|
|
169
|
+
messages.push('_');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// we can use 1st line of stack trace as a message
|
|
173
|
+
messages.push(lines[0]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return stacks.map((stack, i) => ({ stack, message: messages[i] }));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* returns boolean that indicates wether we should
|
|
181
|
+
* ignore the next error event caught by the error event
|
|
182
|
+
* listener.
|
|
183
|
+
*/
|
|
184
|
+
static shouldIgnoreError() {
|
|
185
|
+
return ignoreError > 0;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* transform a log into an error since React hides component errors
|
|
189
|
+
* https://reactjs.org/docs/error-boundaries.html
|
|
190
|
+
*/
|
|
191
|
+
static processErrorLog(errorLog) {
|
|
192
|
+
// handle empty arguments
|
|
193
|
+
if (!errorLog || typeof errorLog !== 'object') {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const { message, stack } = errorLog;
|
|
197
|
+
// for now, we only process error logs if they are a way to describe an error
|
|
198
|
+
if (!stack || !message) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
saveErrorToPagevisit(ERROR_LOG_EVENT_ERROR_TYPE, { message, stack });
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { ErrorMonitor };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { updatePayload } from '../pageVisit/userStep.js';
|
|
2
|
+
import { SOURCE_ATT_NAME, TEXT_ATT_NAME, TAGNAME_ATT_NAME, TYPE_ATT_NAME, KEYBOARD_EVENT_TYPE } from '../constants.js';
|
|
3
|
+
import { EventDebouncer } from '../pageVisit/EventDebouncer.js';
|
|
4
|
+
import { Singleton } from '../types/Monitor.js';
|
|
5
|
+
import { TextInput } from 'react-native';
|
|
6
|
+
|
|
7
|
+
/** @module KeyboardInputMonitor */
|
|
8
|
+
/**
|
|
9
|
+
* KeyboardInputMonitor is a listener class that attaches a
|
|
10
|
+
* keyboard input listener of the document object.
|
|
11
|
+
*/
|
|
12
|
+
class KeyboardInputMonitor extends Singleton {
|
|
13
|
+
/**
|
|
14
|
+
* Begins the monitoring process
|
|
15
|
+
* we currently only monitor two input locations: textarea and input
|
|
16
|
+
*/
|
|
17
|
+
monitor() {
|
|
18
|
+
if (!KeyboardInputMonitor.originalRender) {
|
|
19
|
+
KeyboardInputMonitor.originalRender = TextInput.render;
|
|
20
|
+
TextInput.render = function (props, ...args) {
|
|
21
|
+
/***/
|
|
22
|
+
const onChange = (event) => {
|
|
23
|
+
const currentText = event.nativeEvent.text;
|
|
24
|
+
if (event.target &&
|
|
25
|
+
typeof event.target !== 'number' &&
|
|
26
|
+
'viewConfig' in event.target &&
|
|
27
|
+
event.target.viewConfig) {
|
|
28
|
+
KeyboardInputMonitor.handle(event.target.viewConfig.uiViewClassName, props);
|
|
29
|
+
}
|
|
30
|
+
if (props.onChange)
|
|
31
|
+
props.onChange(event);
|
|
32
|
+
if (props.onChangeText)
|
|
33
|
+
props.onChangeText(currentText);
|
|
34
|
+
};
|
|
35
|
+
const newProps = Object.assign(Object.assign({}, props), { onChange });
|
|
36
|
+
return KeyboardInputMonitor.originalRender.call(this, newProps, ...args);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validates an event
|
|
42
|
+
*/
|
|
43
|
+
static handle(uiViewClassName, props) {
|
|
44
|
+
if (!/(text|input)/i.test(uiViewClassName)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const name = props.placeholder || props.testID;
|
|
48
|
+
if (!name) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
EventDebouncer.getInstance().addEvent(updatePayload({
|
|
52
|
+
[SOURCE_ATT_NAME]: '',
|
|
53
|
+
[TEXT_ATT_NAME]: name,
|
|
54
|
+
[TAGNAME_ATT_NAME]: uiViewClassName.toLowerCase(),
|
|
55
|
+
[TYPE_ATT_NAME]: KEYBOARD_EVENT_TYPE,
|
|
56
|
+
}), KEYBOARD_EVENT_TYPE);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { KeyboardInputMonitor };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { PAGE_EVENTS_WINDOW, PAGE_EVENTS_DOCUMENT, PAGE_EVENT_TYPE } from '../constants.js';
|
|
2
|
+
import { addSafeEventListener } from '../utils/eventlistener.js';
|
|
3
|
+
import { EventDebouncer } from '../pageVisit/EventDebouncer.js';
|
|
4
|
+
import { Singleton } from '../types/Monitor.js';
|
|
5
|
+
|
|
6
|
+
/** @module PageMonitor */
|
|
7
|
+
/** Monitors the page events which we capture and later process */
|
|
8
|
+
class PageMonitor extends Singleton {
|
|
9
|
+
/** Starts monitoring page events on the document */
|
|
10
|
+
monitor() {
|
|
11
|
+
PAGE_EVENTS_WINDOW.forEach(pageEvent => {
|
|
12
|
+
addSafeEventListener(window, pageEvent, this._onPageEventHandle.bind(this), true);
|
|
13
|
+
});
|
|
14
|
+
if (global.document) {
|
|
15
|
+
PAGE_EVENTS_DOCUMENT.forEach(pageEvent => {
|
|
16
|
+
addSafeEventListener(global.document, pageEvent, this._onPageEventHandle.bind(this), true);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Handles a single page event
|
|
22
|
+
* todo: don't think these events exist in RN world
|
|
23
|
+
*/
|
|
24
|
+
_onPageEventHandle(event) {
|
|
25
|
+
if (!event || !event.type) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const payload = {
|
|
29
|
+
type: event.type,
|
|
30
|
+
};
|
|
31
|
+
// Add data if applicable for the event type
|
|
32
|
+
switch (event.type) {
|
|
33
|
+
case 'visibilitychange':
|
|
34
|
+
payload.data = `state: ${this.getDocumentState()}`;
|
|
35
|
+
break;
|
|
36
|
+
case 'readystatechange':
|
|
37
|
+
if (typeof global.document !== 'undefined') {
|
|
38
|
+
payload.data = `state: ${global.document.readyState}`;
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case 'pagehide':
|
|
42
|
+
case 'pageshow':
|
|
43
|
+
case 'load':
|
|
44
|
+
if (event.persisted) {
|
|
45
|
+
payload.data = `persisted: ${event.persisted}`;
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case 'storage':
|
|
49
|
+
if (event.key) {
|
|
50
|
+
payload.data = `key: ${event.key}`;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case 'message':
|
|
54
|
+
case 'messageerror':
|
|
55
|
+
const msgEvent = event;
|
|
56
|
+
if (msgEvent.data && msgEvent.origin) {
|
|
57
|
+
payload.data = `origin: ${msgEvent.origin} size: ${this.getSizeInBytes(msgEvent.data)}`;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case 'hashchange':
|
|
61
|
+
if (event.newURL) {
|
|
62
|
+
payload.data = `newURL: ${event.newURL}`;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
// do nothing
|
|
66
|
+
}
|
|
67
|
+
// storing the page event in the page visit queue
|
|
68
|
+
EventDebouncer.getInstance().addEvent(payload, PAGE_EVENT_TYPE);
|
|
69
|
+
}
|
|
70
|
+
/** Returns document state */
|
|
71
|
+
getDocumentState() {
|
|
72
|
+
if (typeof global.document === 'undefined') {
|
|
73
|
+
return 'passive';
|
|
74
|
+
}
|
|
75
|
+
if (global.document.visibilityState === 'hidden') {
|
|
76
|
+
return 'hidden';
|
|
77
|
+
}
|
|
78
|
+
if (global.document.hasFocus()) {
|
|
79
|
+
return 'active';
|
|
80
|
+
}
|
|
81
|
+
return 'passive';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns object size in bytes
|
|
85
|
+
* @param {} obj
|
|
86
|
+
*/
|
|
87
|
+
getSizeInBytes(obj) {
|
|
88
|
+
let str = obj;
|
|
89
|
+
if (typeof obj !== 'string') {
|
|
90
|
+
// Else, make obj into a string
|
|
91
|
+
str = JSON.stringify(obj);
|
|
92
|
+
}
|
|
93
|
+
// 2B per character in the string
|
|
94
|
+
return str.length * 2;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { PageMonitor };
|