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,284 @@
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/userStep.js';
5
+ import StoredMetrics from '../api/storedMetrics.js';
6
+ import { WHITELIST_TEXT_REGEX_STRING } from '../const_matchers.js';
7
+ import { getBlockedCSSForCurrentDomain, maskTextInput } from '../utils/function.js';
8
+ import { timestampWrapper } from '../utils/date.js';
9
+
10
+ /** @module ClickMonitor */
11
+
12
+
13
+ const maxParentIteration = 5;
14
+
15
+ /** Monitors the clicks which we capture and later process */
16
+ class ClickMonitor {
17
+ /**
18
+ * Creates an instance of the ClickMonitor instance
19
+ */
20
+ constructor() {
21
+ // compile white list regex only once
22
+ this.textCapturedWhiteListRegex = new RegExp(
23
+ WHITELIST_TEXT_REGEX_STRING(),
24
+ 'i',
25
+ );
26
+
27
+ this.htmlIDAllowListRegex = new RegExp(WHITELIST_HTML_ID_TEXT_REGEX, 'i');
28
+ }
29
+
30
+ /** gets the singleton instance
31
+ * @returns {ClickMonitor}
32
+ */
33
+ static getInstance() {
34
+ if (!this.instance) {
35
+ this.instance = new ClickMonitor();
36
+ }
37
+
38
+ return this.instance;
39
+ }
40
+
41
+ /** Starts monitoring clicks on the document */
42
+ monitorClicks() {
43
+ const onClickHandler = this._onClickHandle.bind(this);
44
+ // addSafeEventListener(window, 'click', onClickHandler, true);
45
+
46
+ if (!Pressability.prototype.originalCreateEventHandlers) {
47
+ Pressability.prototype.originalCreateEventHandlers =
48
+ Pressability.prototype.getEventHandlers;
49
+
50
+ Pressability.prototype.getEventHandlers = function () {
51
+ const ehs =
52
+ Pressability.prototype.originalCreateEventHandlers.call(this);
53
+
54
+ return Object.fromEntries(
55
+ Object.entries(ehs).map(([key, handler]) => [
56
+ key,
57
+ (event, ...args) => {
58
+ if (key === 'onResponderRelease') {
59
+ onClickHandler(event);
60
+ }
61
+ return handler(event, ...args);
62
+ },
63
+ ]),
64
+ );
65
+ };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Handles a single click event
71
+ * @param {} event
72
+ */
73
+ _onClickHandle(event) {
74
+ const blockedCSS = getBlockedCSSForCurrentDomain();
75
+ if (event) {
76
+ const { _targetInst: target } = event;
77
+ const targetClassName = target.elementType;
78
+
79
+ let text = '';
80
+ // if the tag name of the src element is image, then we need
81
+ // to process the image name, else we need to get the textual content
82
+ // todo process images
83
+
84
+ text = this._getTextualContentFromEl(target, false, blockedCSS);
85
+
86
+ let textFromElement = this._trimText(text);
87
+
88
+ let tagName = '';
89
+ if (targetClassName) {
90
+ tagName = targetClassName.toLowerCase();
91
+ }
92
+
93
+ // id of element
94
+ let hid = target.memoizedProps.testID || '';
95
+ // in some bizarre cases, the html id of an element gets overriden
96
+ // to contain jquery objects. If the hid is an object, it's of no
97
+ // use to us.
98
+ if (typeof hid !== 'string') {
99
+ hid = '';
100
+ }
101
+ // if we find that the text matches analytic data used
102
+ // to find checkout starts, add to cart clicks, etc.
103
+ // we do not mask it.
104
+ if (
105
+ !this.textCapturedWhiteListRegex.test(textFromElement) &&
106
+ !this.htmlIDAllowListRegex.test(hid)
107
+ ) {
108
+ if (tagName === 'input') {
109
+ if (
110
+ event.type &&
111
+ (event.type === 'button' || event.type === 'submit')
112
+ ) ; else {
113
+ textFromElement = '*';
114
+ }
115
+ } else if (tagName === 'textarea') {
116
+ textFromElement = '*';
117
+ }
118
+ }
119
+
120
+ textFromElement = maskTextInput(textFromElement);
121
+ const tPayload = {
122
+ [SOURCE_ATT_NAME]: '',
123
+ [TEXT_ATT_NAME]: textFromElement,
124
+ [TAGNAME_ATT_NAME]: tagName,
125
+ [HTMLID_ATT_NAME]: hid,
126
+ [TYPE_ATT_NAME]: CLICK_EVENT_TYPE,
127
+ [CSS_CLASS_ATT_NAME]: '',
128
+ };
129
+
130
+ StoredMetrics.getInstance().addPvClick();
131
+
132
+ PageVisit.getInstance().addPageVisitEvents(
133
+ [
134
+ {
135
+ event: updatePayload(tPayload),
136
+ occurredAt: new Date(timestampWrapper(Date.now())).toISOString(),
137
+ },
138
+ ],
139
+ USERSTEP_EVENT_TYPE,
140
+ );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * parseTextFromParentElement will parse the parents of an element to try
146
+ * and find textual content if no text can be extracted from the clicked element
147
+ * @param {} element
148
+ * @param {Array.<String>} blockedCSS
149
+ */
150
+ _parseTextFromParentElement(element, blockedCSS) {
151
+ let iteratableElement = element;
152
+ const parentElements = [];
153
+ let parentIterations = 0;
154
+ while (iteratableElement) {
155
+ if (
156
+ parentIterations >= maxParentIteration ||
157
+ !iteratableElement.parentNode
158
+ ) {
159
+ break;
160
+ }
161
+ iteratableElement = iteratableElement.parentNode;
162
+ parentElements.push(iteratableElement);
163
+ parentIterations += 1;
164
+ }
165
+
166
+ for (let i = 0; i < parentElements.length; i += 1) {
167
+ const el = parentElements[i];
168
+ // we only get the text content if the clicked element has a button parent
169
+ if (el && el.tagName === 'BUTTON') {
170
+ // we disable the linter because we use 1 level recursion.
171
+ // eslint-disable-next-line no-use-before-define
172
+ return this._getTextualContentFromEl(el, false, blockedCSS);
173
+ }
174
+ }
175
+
176
+ return '';
177
+ }
178
+
179
+ /** Gets the textual content from an element, if any
180
+ * @param {} element
181
+ * @param {} parseParent
182
+ * @param {Array.<String>} blockedCSS
183
+ */
184
+ _getTextualContentFromEl(element, parseParent, blockedCSS) {
185
+ return this._parseInnerContent(
186
+ element,
187
+ '',
188
+ 100,
189
+ { value: 0, limit: 100 },
190
+ blockedCSS,
191
+ );
192
+ }
193
+
194
+ /** Parse and trim text
195
+ * @param {} text
196
+ */
197
+ _trimText(text) {
198
+ // regex to replace all whitespace with a single space and trim text
199
+ let parsedText = text.trim().replace(/\s+/g, ' ');
200
+
201
+ // if there is text that is longer than average sentence length,
202
+ // chances are that it is not a valid text so we need to further process it
203
+ if (parsedText.length > 100) {
204
+ const index = parsedText.lastIndexOf(' ', 97);
205
+ // We can not chop the text at exactly 97 characters since chopping in the
206
+ // middle of an encoded value may cause deserialization issues. Chop at the
207
+ // last ' ' character before the 97th character.
208
+ if (index > 0) {
209
+ parsedText = `${parsedText.substring(0, index)}...`;
210
+ } else {
211
+ // If the are no ' ' characters then the text is likely not valid so just
212
+ // return '...'.
213
+ parsedText = '...';
214
+ }
215
+ }
216
+ return parsedText;
217
+ }
218
+
219
+ /**
220
+ * Recursively parses element's inner content and masks blocked css classes
221
+ * @param {HTMLElement} element
222
+ * @param {String} text
223
+ * @param {Number} textLimit
224
+ * @param {Object} counter
225
+ * @param {Array.<String>} blockedCSS
226
+ */
227
+ _parseInnerContent(element, text, textLimit, counter, blockedCSS) {
228
+ /* eslint-disable no-restricted-syntax */
229
+ /* eslint-disable no-param-reassign */
230
+
231
+ if (text.length >= textLimit) {
232
+ return text;
233
+ }
234
+
235
+ if (counter.value >= counter.limit) {
236
+ return text;
237
+ }
238
+
239
+ counter.value += 1;
240
+
241
+ if (blockedCSS.includes(element.memoizedProps.testID)) {
242
+ return `${text}${text ? ' ' : ''}*`;
243
+ }
244
+
245
+ const walk = node => {
246
+ if (typeof node.memoizedProps === 'string') {
247
+ text = this._parseAndAppendText(text, [node.memoizedProps]);
248
+ if (text.length >= textLimit) {
249
+ return;
250
+ }
251
+ }
252
+ if (node.child) {
253
+ walk(node.child);
254
+ }
255
+ };
256
+ walk(element);
257
+
258
+ return text;
259
+ }
260
+
261
+ /**
262
+ * normalize value and append to the resulting text if not empty
263
+ * @param {String} text
264
+ * @param {Array.<String>} values
265
+ */
266
+ _parseAndAppendText(text, values) {
267
+ const goodValues = [];
268
+ for (const v of values) {
269
+ if (Number.isFinite(v) || typeof v === 'string') {
270
+ goodValues.push(v);
271
+ }
272
+ }
273
+
274
+ for (let value of goodValues) {
275
+ value = `${value}`.trim().replace(/\s+/g, ' ');
276
+ if (value.length > 0) {
277
+ return text + (text ? ' ' : '') + value;
278
+ }
279
+ }
280
+ return text;
281
+ }
282
+ }
283
+
284
+ export { ClickMonitor };
@@ -0,0 +1,174 @@
1
+ import { GET_ATTRIBUTE_SELECTORS } from '../constants.js';
2
+ import InputManager from '../api/inputManager.js';
3
+
4
+ /** @module ElementMonitor */
5
+
6
+ /** Monitors the elements matching query selectors and passes them to the InputManager */
7
+ class ElementMonitor {
8
+ /** gets the singleton instance */
9
+ static getInstance() {
10
+ if (!this.instance) {
11
+ this.instance = new ElementMonitor();
12
+ }
13
+
14
+ return this.instance;
15
+ }
16
+
17
+ /**
18
+ * Safely calls `querySelectorAll` on the given node and returns an array of
19
+ * matching child elements. If `querySelectorAll` returns a falsy value,
20
+ * an empty array is returned.
21
+ *
22
+ * @private
23
+ * @param {Node} node - The node on which to call `querySelectorAll`.
24
+ * @param {string} selector - The CSS selector used to match child elements.
25
+ * @returns {Array<Element>} An array of matching child elements.
26
+ */
27
+ _safeQueryAll(node, selector) {
28
+ const matchingChildElements = node.querySelectorAll(selector);
29
+ if (!matchingChildElements) {
30
+ return [];
31
+ }
32
+
33
+ return Array.from(matchingChildElements);
34
+ }
35
+
36
+ /**
37
+ * Processes the given elements by adding their custom attributes to the InputManager.
38
+ *
39
+ * @private
40
+ * @param {NodeList|Array} elements - The list of elements to be processed.
41
+ * @param {string} selector - The selector corresponding to the elements being processed.
42
+ */
43
+ _processMatchingElements(elements, selector) {
44
+ elements.forEach(element => {
45
+ if (!element) {
46
+ return;
47
+ }
48
+
49
+ const selectorText = element.textContent;
50
+
51
+ if (!selectorText) {
52
+ return;
53
+ }
54
+
55
+ InputManager.getInstance()._addCustomAttribute(selector, selectorText);
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Finds and processes matching elements within the added nodes by checking if
61
+ * they match any of the attribute selectors from GET_ATTRIBUTE_SELECTORS().
62
+ * If a node or any of its descendants match, their custom attributes are added
63
+ * to the InputManager.
64
+ *
65
+ * @private
66
+ * @param {NodeList|Array} addedNodes - The list of nodes that were added or
67
+ * modified in the DOM. Each node in the list will be checked against the
68
+ * attribute selectors, and their matching descendants will also be processed.
69
+ */
70
+ _findAndAddMatchingElementsInNodes(addedNodes) {
71
+ const attributeSelectorNames = Object.keys(GET_ATTRIBUTE_SELECTORS());
72
+ attributeSelectorNames.forEach(name => {
73
+ const selector = GET_ATTRIBUTE_SELECTORS()[name];
74
+ if (!selector) {
75
+ return;
76
+ }
77
+
78
+ addedNodes.forEach(node => {
79
+ // move to the next node its not an element node
80
+ if (node.nodeType !== Node.ELEMENT_NODE) {
81
+ return;
82
+ }
83
+
84
+ let matchingElements = [];
85
+
86
+ if (node.matches(selector)) {
87
+ matchingElements.push(node);
88
+ }
89
+
90
+ const matchingChildElements = this._safeQueryAll(node, selector);
91
+ matchingElements = matchingElements.concat(matchingChildElements);
92
+
93
+ this._processMatchingElements(matchingElements, name);
94
+ });
95
+ });
96
+ }
97
+
98
+ /** Sets up a MutationObserver to monitor added elements and attribute changes */
99
+ _setupMutationObserver() {
100
+ const observer = new MutationObserver(mutations => {
101
+ mutations.forEach(mutation => {
102
+ // Handle childList mutations
103
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
104
+ this._findAndAddMatchingElementsInNodes(mutation.addedNodes);
105
+ }
106
+
107
+ // Handle attribute mutations
108
+ if (mutation.type === 'attributes') {
109
+ const targetNode = mutation.target;
110
+ if (targetNode.nodeType === Node.ELEMENT_NODE) {
111
+ this._findAndAddMatchingElementsInNodes([targetNode]);
112
+ }
113
+ }
114
+ });
115
+ });
116
+
117
+ // the observer will get called when children are added and changed
118
+ const observerConfig = { childList: true, subtree: true, attributes: true };
119
+ observer.observe(document.documentElement, observerConfig);
120
+ this.observer = observer;
121
+ }
122
+
123
+ /** Finds and adds matching elements to the InputManager */
124
+ _findAndAddMatchingElements() {
125
+ const attributeSelectorNames = Object.keys(GET_ATTRIBUTE_SELECTORS());
126
+ attributeSelectorNames.forEach(name => {
127
+ const selector = GET_ATTRIBUTE_SELECTORS()[name];
128
+ if (!selector) {
129
+ return;
130
+ }
131
+ const elements = this._safeQueryAll(document, selector);
132
+ this._processMatchingElements(elements, name);
133
+ });
134
+ }
135
+
136
+ /** Starts monitoring elements matching query selectors */
137
+ monitor() {
138
+ const attributeNames = Object.keys(GET_ATTRIBUTE_SELECTORS());
139
+ // if we have no attributes to look for, do not setup the listeners
140
+ if (attributeNames.length === 0) {
141
+ return;
142
+ }
143
+ this._findAndAddMatchingElements();
144
+ if (typeof MutationObserver !== 'undefined') {
145
+ this._setupMutationObserver();
146
+ } else {
147
+ // Fallback using setInterval if MutationObserver is not supported
148
+ this.interval = setInterval(() => {
149
+ this._findAndAddMatchingElements();
150
+ }, 5000); // Check for changes every N seconds
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Disconnects the MutationObserver instance, if it exists and has a valid disconnect method.
156
+ * This is needed for testing since we want to properly tear down mutation listeners
157
+ * @private
158
+ */
159
+ _disconnectObserver() {
160
+ if (
161
+ this.observer &&
162
+ this.observer.disconnect &&
163
+ typeof this.observer.disconnect === 'function'
164
+ ) {
165
+ this.observer.disconnect();
166
+ }
167
+
168
+ if (this.interval) {
169
+ clearInterval(this.interval);
170
+ }
171
+ }
172
+ }
173
+
174
+ export { ElementMonitor };