noibu-react-native 0.1.2 → 0.2.0

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 (52) hide show
  1. package/android/.gitignore +13 -0
  2. package/android/build.gradle +79 -0
  3. package/android/gradle.properties +7 -0
  4. package/android/src/main/AndroidManifest.xml +5 -0
  5. package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt +107 -0
  6. package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayPackage.kt +17 -0
  7. package/dist/api/clientConfig.d.ts +6 -7
  8. package/dist/api/clientConfig.js +14 -16
  9. package/dist/api/helpCode.d.ts +29 -0
  10. package/dist/api/inputManager.d.ts +87 -0
  11. package/dist/api/metroplexSocket.d.ts +156 -0
  12. package/dist/api/metroplexSocket.js +662 -815
  13. package/dist/api/storedMetrics.d.ts +73 -0
  14. package/dist/api/storedPageVisit.d.ts +49 -0
  15. package/dist/const_matchers.d.ts +1 -0
  16. package/dist/constants.d.ts +10 -1
  17. package/dist/constants.js +19 -2
  18. package/dist/entry/index.d.ts +1 -1
  19. package/dist/entry/init.d.ts +1 -1
  20. package/dist/entry/init.js +10 -6
  21. package/dist/monitors/appNavigationMonitor.js +3 -2
  22. package/dist/monitors/clickMonitor.d.ts +44 -0
  23. package/dist/monitors/gqlErrorValidator.d.ts +82 -0
  24. package/dist/monitors/httpDataBundler.d.ts +161 -0
  25. package/dist/monitors/inputMonitor.d.ts +34 -0
  26. package/dist/monitors/inputMonitor.js +5 -0
  27. package/dist/monitors/integrations/react-native-navigation-integration.d.ts +1 -2
  28. package/dist/monitors/keyboardInputMonitor.d.ts +17 -0
  29. package/dist/monitors/pageMonitor.d.ts +22 -0
  30. package/dist/monitors/requestMonitor.d.ts +10 -0
  31. package/dist/pageVisit/pageVisit.d.ts +52 -0
  32. package/dist/pageVisit/pageVisit.js +9 -2
  33. package/dist/pageVisit/pageVisitEventError.d.ts +15 -0
  34. package/dist/pageVisit/pageVisitEventHTTP.d.ts +18 -0
  35. package/dist/pageVisit/userStep.d.ts +5 -0
  36. package/dist/react/ErrorBoundary.js +17 -9
  37. package/dist/sessionRecorder/nativeSessionRecorderSubscription.d.ts +64 -0
  38. package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +58 -0
  39. package/dist/sessionRecorder/sessionRecorder.d.ts +60 -0
  40. package/dist/sessionRecorder/sessionRecorder.js +287 -0
  41. package/dist/sessionRecorder/types.d.ts +91 -0
  42. package/dist/types/NavigationIntegration.d.ts +1 -2
  43. package/dist/types/StoredPageVisit.types.d.ts +54 -0
  44. package/dist/types/globals.d.ts +2 -1
  45. package/dist/utils/date.d.ts +6 -0
  46. package/dist/utils/eventlistener.d.ts +8 -0
  47. package/dist/utils/eventlistener.js +2 -2
  48. package/dist/utils/function.d.ts +6 -3
  49. package/dist/utils/function.js +23 -10
  50. package/dist/utils/log.d.ts +0 -1
  51. package/dist/utils/object.d.ts +2 -2
  52. package/package.json +15 -6
@@ -0,0 +1,287 @@
1
+ import { strToU8, zlibSync, strFromU8 } from '../node_modules/fflate/esm/browser.js';
2
+ import { LogLevel, initialize, subscribeToNativeEvent } from './nativeSessionRecorderSubscription.js';
3
+ import StoredMetrics from '../api/storedMetrics.js';
4
+ import ClientConfig from '../api/clientConfig.js';
5
+ import { stringifyJSON } from '../utils/function.js';
6
+ import { SEVERITY, POST_METRICS_EVENT_NAME, MAX_TIME_FOR_RECORDER_USER_EVENTS, MAX_RECORDER_EVENT_BUFFER, MAX_TIME_FOR_UNSENT_DATA_MILLIS, VIDEO_FRAG_ATT_NAME, PV_SEQ_ATT_NAME, LENGTH_ATT_NAME, CSS_URLS_ATT_NAME, VIDEO_METROPLEX_TYPE, PAGE_VISIT_VID_FRAG_ATT_NAME } from '../constants.js';
7
+ import MetroplexSocket from '../api/metroplexSocket.js';
8
+ import { addSafeEventListener } from '../utils/eventlistener.js';
9
+ import { noibuLog } from '../utils/log.js';
10
+
11
+ /** Singleton class to record user sessions */
12
+ class SessionRecorder {
13
+ static instance;
14
+ eventBuffer;
15
+ vfCounter;
16
+ didSetupRecorder;
17
+ isVideoLengthNegativeInvalid;
18
+ lastFragPostTimestamp;
19
+ pauseTimeout;
20
+ lastRecordedTimestamp;
21
+ firstRecordedTimestamp;
22
+ recordStopper;
23
+ freezingEvents = false;
24
+ /**
25
+ * Creates an instance of the session recorder
26
+ */
27
+ constructor() {
28
+ this.eventBuffer = [];
29
+ this.vfCounter = 0;
30
+ this.didSetupRecorder = false;
31
+ this.recordStopper = null;
32
+ this.firstRecordedTimestamp = null;
33
+ this.lastRecordedTimestamp = null;
34
+ this.isVideoLengthNegativeInvalid = false;
35
+ this.lastFragPostTimestamp = Date.now();
36
+ this.pauseTimeout = null;
37
+ this.setupUnloadHandler();
38
+ this.setupPostMetricsHandler();
39
+ }
40
+ /**
41
+ * Setups the SessionRecorder instance for usage
42
+ */
43
+ static getInstance() {
44
+ if (!this.instance) {
45
+ const nativeSessionRecorderConfig = {
46
+ logLevel: LogLevel.Verbose,
47
+ };
48
+ initialize('abc1234', nativeSessionRecorderConfig);
49
+ this.instance = new SessionRecorder();
50
+ // todo handle RN clicks
51
+ addSafeEventListener(window, 'click', () => {
52
+ this.instance.handleFragPost();
53
+ });
54
+ }
55
+ return this.instance;
56
+ }
57
+ /** Sets up the page hide handler to try to push remaining video events */
58
+ setupUnloadHandler() {
59
+ // todo handle React Native app background / foreground events
60
+ ['pagehide', 'visibilitychange'].forEach(evt => addSafeEventListener(window, evt, () => {
61
+ // pagehide handler
62
+ if (evt === 'pagehide') {
63
+ this.handleFragPost();
64
+ }
65
+ }));
66
+ }
67
+ /** Sets up the post metrics handler to potentially log a debug message */
68
+ setupPostMetricsHandler() {
69
+ addSafeEventListener(window, POST_METRICS_EVENT_NAME, (e) => {
70
+ // Get the event name that triggered postMetrics from the custom event
71
+ const eventName = e.detail;
72
+ // Calculate the total expected video length
73
+ const totalVideoTime = this.lastRecordedTimestamp === null ||
74
+ this.firstRecordedTimestamp === null
75
+ ? 0
76
+ : this.lastRecordedTimestamp - this.firstRecordedTimestamp;
77
+ const sessionLength = MetroplexSocket.getInstance().sessionLength
78
+ ? MetroplexSocket.getInstance().sessionLength
79
+ : 0;
80
+ // If there is a large retry message queue, log a debug message.
81
+ if (MetroplexSocket.getInstance().retryMessageQueue.length > 100) {
82
+ // if client was disabled (due to inactive or otherwise) enable briefly so the
83
+ // debug message gets through
84
+ const clientDisabled = ClientConfig.getInstance().isClientDisabled;
85
+ ClientConfig.getInstance().isClientDisabled = false;
86
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient({
87
+ eventName,
88
+ totalVideoTime,
89
+ sessionLength,
90
+ }, clientDisabled, SEVERITY.warn);
91
+ }
92
+ });
93
+ }
94
+ /**
95
+ * Starts recording the user session
96
+ */
97
+ async recordUserSession() {
98
+ // check if inactive before starting any recording
99
+ noibuLog('recordUserSession');
100
+ if ((await MetroplexSocket.getInstance().closeIfInactive()) ||
101
+ StoredMetrics.getInstance().didCutVideo) {
102
+ return;
103
+ }
104
+ // making sure we are not attempting to call this method
105
+ // multiple times.
106
+ if (this.didSetupRecorder) {
107
+ return;
108
+ }
109
+ StoredMetrics.getInstance().setDidStartVideo();
110
+ this.recordStopper = subscribeToNativeEvent(this.handleRecorderEvent.bind(this));
111
+ this.didSetupRecorder = true;
112
+ }
113
+ /**
114
+ * handleNewRRwebEvent will process each upcoming.
115
+ * rrweb event. It will make sure that the current buffer
116
+ * is updated with the latest events and post the contents
117
+ * of the buffer if it exceeds max size
118
+ */
119
+ async handleRecorderEvent(recorderEvent) {
120
+ const timestamp = Date.now();
121
+ // check if inactive before any processing
122
+ if ((await MetroplexSocket.getInstance().closeIfInactive()) ||
123
+ StoredMetrics.getInstance().didCutVideo) {
124
+ this.freeze();
125
+ return;
126
+ }
127
+ // determine if timeout should be extended based on event/source type
128
+ // event type 3 is an incremental snapshot
129
+ // event data source 0 is a mutation (all other data sources are user events)
130
+ if (this.pauseTimeout) {
131
+ // received a user event, extend the timeout
132
+ clearTimeout(this.pauseTimeout);
133
+ this.freezingEvents = false;
134
+ }
135
+ this.pauseTimeout = setTimeout(() => {
136
+ // stop recording page mutations after 2s of inactivity
137
+ // otherwise sites with many mutations will hit max video size
138
+ // in a short amount of time without any user events
139
+ this.freezingEvents = true;
140
+ // freezePage stops emitting events until the next user event is received
141
+ this.freeze();
142
+ }, MAX_TIME_FOR_RECORDER_USER_EVENTS);
143
+ // Set the first recorded timestamp if it hasn't been set yet.
144
+ // We usually only want this to be set once as the first recorded timestamp
145
+ // should not change.
146
+ if (!this.firstRecordedTimestamp) {
147
+ this.firstRecordedTimestamp = timestamp;
148
+ }
149
+ // Set the last recorded timestamp if it hasn't been set yet or a newly received rrweb
150
+ // event has a more recent timestamp than the last recorded timestamp.
151
+ if (!this.lastRecordedTimestamp || timestamp > this.lastRecordedTimestamp) {
152
+ this.lastRecordedTimestamp = timestamp;
153
+ }
154
+ // Checks if we've gone back in time for some reason.
155
+ // If we have, adjust our data accordingly to ensure we don't mess up
156
+ // the metrics 'exp_vid_len' data.
157
+ // If we don't adjust for time, we assume that the expected video length is
158
+ // the difference between the first recorded timestamp and the last recorded timestamp.
159
+ if (this.firstRecordedTimestamp &&
160
+ timestamp < this.firstRecordedTimestamp) {
161
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Detected time rewind. Client has been disabled.`, true, SEVERITY.error, true);
162
+ return;
163
+ }
164
+ const packedEvent = await this.pack(recorderEvent.message);
165
+ // Buffer the event for sending to metroplex
166
+ this.eventBuffer.push(packedEvent);
167
+ // Check if the event was a click or a double click. This is true if the root type is
168
+ // incremental snapshot (3) and the data source is mouse interaction data (2).
169
+ // Finally, we capture a click (2) or double click (4) event.
170
+ // todo if there are clicks, call StoredMetrics.getInstance().addVideoClick(); for each click
171
+ const now = Date.now();
172
+ const delta = now - this.lastFragPostTimestamp;
173
+ if (this.eventBuffer.length >= MAX_RECORDER_EVENT_BUFFER ||
174
+ delta > MAX_TIME_FOR_UNSENT_DATA_MILLIS) {
175
+ this.handleFragPost();
176
+ }
177
+ }
178
+ /**
179
+ * Compress event
180
+ */
181
+ async pack(recorderEvent) {
182
+ // return JSON.stringify(recorderEvent);
183
+ return SessionRecorder.compress(recorderEvent);
184
+ }
185
+ static compress(snapshot) {
186
+ const uncompressedString = stringifyJSON(snapshot);
187
+ const uncompressedData = strToU8(uncompressedString);
188
+ const compressedData = zlibSync(uncompressedData, { level: 1 });
189
+ const compressedString = strFromU8(compressedData, true);
190
+ return compressedString;
191
+ }
192
+ /** builds a log message with debug info
193
+ */
194
+ buildDebugMessage(eventName, totalVideoTime, sessionLength) {
195
+ return JSON.stringify({ eventName, totalVideoTime, sessionLength });
196
+ }
197
+ /**
198
+ * handleFragPost communicates with the Metroplex socket
199
+ * to post video fragments when needed. It also handles
200
+ * necessary management of the buffer and it's related
201
+ * variables
202
+ */
203
+ async handleFragPost() {
204
+ // check if inactive before any processing
205
+ if (await MetroplexSocket.getInstance().closeIfInactive()) {
206
+ return;
207
+ }
208
+ if (!this.didSetupRecorder) {
209
+ return;
210
+ }
211
+ if (this.eventBuffer.length === 0) {
212
+ return;
213
+ }
214
+ try {
215
+ let totalVideoTime = 0;
216
+ // checking if we have those values set in the first place
217
+ if (this.firstRecordedTimestamp &&
218
+ this.lastRecordedTimestamp &&
219
+ !this.isVideoLengthNegativeInvalid) {
220
+ totalVideoTime =
221
+ this.lastRecordedTimestamp - this.firstRecordedTimestamp;
222
+ }
223
+ // In the past we have seen the video LengthMS field to be negative
224
+ // and bigger than the long limit of scala. Which is less than the
225
+ // safe integer limit of js.
226
+ if (!this.isVideoLengthNegativeInvalid &&
227
+ (totalVideoTime < 0 || totalVideoTime >= Number.MAX_SAFE_INTEGER)) {
228
+ // we log an error to know if this is still happening
229
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`video lengthMS is invalid: ${totalVideoTime}, ` +
230
+ `start time: ${this.firstRecordedTimestamp}, ` +
231
+ `end time: ${this.lastRecordedTimestamp}`, false, SEVERITY.error);
232
+ this.isVideoLengthNegativeInvalid = true;
233
+ totalVideoTime = 0;
234
+ }
235
+ this.vfCounter += 1;
236
+ const videoFragment = MetroplexSocket.getInstance().addEndTimeToPayload({
237
+ // single string for the video content. This gets converted to bytes
238
+ // when being sent to metroplex which we will then unmarshall into
239
+ // a struct to parse it's inner urls
240
+ // If stringifying this event buffer takes too long consider using a service worker
241
+ [VIDEO_FRAG_ATT_NAME]: stringifyJSON(this.eventBuffer),
242
+ // Send the sequence number but don't send the expected length since that is sent as
243
+ // part of the last stored metrics data
244
+ [PV_SEQ_ATT_NAME]: this.vfCounter,
245
+ [LENGTH_ATT_NAME]: totalVideoTime,
246
+ [CSS_URLS_ATT_NAME]: [],
247
+ }, false);
248
+ StoredMetrics.getInstance().addVideoFragData(this.vfCounter, totalVideoTime);
249
+ // constructing a client message that metroplex knows how to handle.
250
+ await MetroplexSocket.getInstance().sendMessage(VIDEO_METROPLEX_TYPE, {
251
+ [PAGE_VISIT_VID_FRAG_ATT_NAME]: videoFragment,
252
+ });
253
+ this.lastFragPostTimestamp = Date.now();
254
+ }
255
+ catch (err) {
256
+ // letting collect know we are closing the rrweb listener
257
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`video frag socket closed with err: ${err.message}`, false, SEVERITY.error);
258
+ // if we detect an error in the frag posting, we stop recording
259
+ // the video
260
+ this.freeze();
261
+ }
262
+ this.eventBuffer = [];
263
+ }
264
+ /**
265
+ * unfreeze forcefully resumes recording events in case it was frozen
266
+ * waiting for user events
267
+ */
268
+ async unfreeze() {
269
+ if (this.freezingEvents) {
270
+ this.didSetupRecorder = false;
271
+ await this.recordUserSession();
272
+ }
273
+ }
274
+ /** stops recording */
275
+ freeze() {
276
+ if (this.recordStopper) {
277
+ try {
278
+ this.recordStopper();
279
+ }
280
+ catch (e) {
281
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error during handleFragPost in recordStopper: ${e}`, false, SEVERITY.error);
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ export { SessionRecorder as default };
@@ -0,0 +1,91 @@
1
+ type Color = {
2
+ a: number;
3
+ b: number;
4
+ r: number;
5
+ g: number;
6
+ };
7
+ type Paint = {
8
+ strokeJoin: number;
9
+ strokeWidth: number;
10
+ strokeCap: number;
11
+ color: Color;
12
+ dither: boolean;
13
+ blendMode: number;
14
+ style: number;
15
+ antiAlias: boolean;
16
+ strokeMiter: number;
17
+ };
18
+ type Rect = {
19
+ top: number;
20
+ left: number;
21
+ bottom: number;
22
+ right: number;
23
+ };
24
+ type Command = {
25
+ name?: string;
26
+ id?: number;
27
+ type: string;
28
+ isClipRectSource?: boolean;
29
+ rect?: Rect;
30
+ paintIndex?: number;
31
+ op?: number;
32
+ antiAlias?: boolean;
33
+ matrix?: number[];
34
+ };
35
+ type ViewNode = {
36
+ viewX: number;
37
+ visible: boolean;
38
+ processedText$delegate: {
39
+ _value: any;
40
+ initializer: any;
41
+ };
42
+ viewY: number;
43
+ viewWidth: number;
44
+ clickable: boolean;
45
+ viewHeight: number;
46
+ isMasked: boolean;
47
+ type: string;
48
+ renderNodeId: number;
49
+ isWebView: boolean;
50
+ ignoreClicks: boolean;
51
+ children: ViewNode[];
52
+ width: number;
53
+ x: number;
54
+ y: number;
55
+ id: number;
56
+ text: string;
57
+ height: number;
58
+ backgroundColor?: number;
59
+ };
60
+ type ViewHierarchy = {
61
+ root: ViewNode;
62
+ visibleFragments: any[];
63
+ timestamp: number;
64
+ };
65
+ type SubPicture = {
66
+ subPictures: any[];
67
+ images: any[];
68
+ screenWidth: number;
69
+ textBlobs: any[];
70
+ density: number;
71
+ vertices: any[];
72
+ screenHeight: number;
73
+ activityName: string;
74
+ paints: Paint[];
75
+ typefaces: any[];
76
+ viewHierarchy: ViewHierarchy;
77
+ paths: any[];
78
+ activityHashCode: number;
79
+ commands: Command[];
80
+ timestamp: number;
81
+ };
82
+ export type RecorderEvent = {
83
+ message: NativeFrames;
84
+ };
85
+ export type NativeFrames = {
86
+ p: (number | boolean | SubPicture)[][];
87
+ a: (number[] | (number | string[] | string | string[])[])[];
88
+ e: (string | number)[];
89
+ };
90
+ export type UnsubscribeFn = () => void;
91
+ export {};
@@ -1,7 +1,6 @@
1
- import type { NavigationDelegate } from 'react-native-navigation/lib/dist/src/NavigationDelegate';
2
1
  /**
3
2
  * interface enforces constructor signature
4
3
  */
5
4
  export interface NavigationIntegration {
6
- register(navigation: NavigationDelegate, onNavigation: (breadcrumbs: string[]) => void): void;
5
+ register(navigation: any, onNavigation: (breadcrumbs: string[]) => void): void;
7
6
  }
@@ -0,0 +1,54 @@
1
+ import { BROWSER_ID_ATT_NAME, COLLECT_VER_ATT_NAME, CONN_COUNT_ATT_NAME, CSS_URLS_ATT_NAME, END_AT_ATT_NAME, IS_LAST_ATT_NAME, LANG_ATT_NAME, LENGTH_ATT_NAME, METROPLEX_SOCKET_INSTANCE_ID_ATT_NAME, ON_URL_ATT_NAME, PAGE_GROUPS_ATT_NAME, PAGE_TITLE_ATT_NAME, PAGE_VISIT_INFORMATION_ATT_NAME, PAGE_VISIT_PART_ATT_NAME, PAGE_VISIT_VID_FRAG_ATT_NAME, PV_EVENTS_ATT_NAME, PV_EXP_PART_COUNTER_ATT_NAME, PV_EXP_VF_SEQ_ATT_NAME, PV_ID_ATT_NAME, PV_PART_COUNTER_ATT_NAME, PV_SEQ_ATT_NAME, REF_URL_ATT_NAME, SCRIPT_ID_ATT_NAME, SCRIPT_INSTANCE_ID_ATT_NAME, SEQ_NUM_ATT_NAME, SOCKET_INSTANCE_ID_ATT_NAME, STARTED_AT_ATT_NAME, VIDEO_FRAG_ATT_NAME, VIDEO_METROPLEX_TYPE } from '../constants';
2
+ export interface PageVisitPart {
3
+ [PV_PART_COUNTER_ATT_NAME]: number;
4
+ [END_AT_ATT_NAME]: number;
5
+ [PV_EXP_VF_SEQ_ATT_NAME]?: number;
6
+ [PV_EXP_PART_COUNTER_ATT_NAME]?: number;
7
+ [PV_EVENTS_ATT_NAME]: Array<any>;
8
+ [SEQ_NUM_ATT_NAME]: number;
9
+ }
10
+ export interface PageVisitInfo {
11
+ [BROWSER_ID_ATT_NAME]: string;
12
+ [PV_ID_ATT_NAME]: string;
13
+ [VIDEO_METROPLEX_TYPE]: number;
14
+ [SCRIPT_ID_ATT_NAME]: string;
15
+ [SCRIPT_INSTANCE_ID_ATT_NAME]: string;
16
+ [METROPLEX_SOCKET_INSTANCE_ID_ATT_NAME]: string;
17
+ [SOCKET_INSTANCE_ID_ATT_NAME]: string;
18
+ [PV_SEQ_ATT_NAME]: number;
19
+ [IS_LAST_ATT_NAME]: boolean;
20
+ [CONN_COUNT_ATT_NAME]: number;
21
+ [ON_URL_ATT_NAME]: string;
22
+ [PAGE_GROUPS_ATT_NAME]: Array<string>;
23
+ [PAGE_TITLE_ATT_NAME]: string;
24
+ [REF_URL_ATT_NAME]: string;
25
+ [STARTED_AT_ATT_NAME]: string;
26
+ [COLLECT_VER_ATT_NAME]: number;
27
+ [LANG_ATT_NAME]?: string;
28
+ }
29
+ export interface PageVisitPartsBase {
30
+ pageVisitInfo: PageVisitInfo;
31
+ pageVisitFrags: Array<PageVisitPart>;
32
+ }
33
+ export interface PageVisitPartsForStorage extends PageVisitPartsBase {
34
+ timestamp?: Date;
35
+ }
36
+ export interface PageVisitPartsFromStorage extends PageVisitPartsBase {
37
+ timestamp?: string;
38
+ }
39
+ export interface VideoFrag {
40
+ [VIDEO_FRAG_ATT_NAME]: string;
41
+ [PV_SEQ_ATT_NAME]: number;
42
+ [END_AT_ATT_NAME]: number;
43
+ [LENGTH_ATT_NAME]: number;
44
+ [SEQ_NUM_ATT_NAME]: number;
45
+ [CSS_URLS_ATT_NAME]: string[];
46
+ }
47
+ export interface VideoMetroplexMessage {
48
+ [PAGE_VISIT_VID_FRAG_ATT_NAME]: VideoFrag;
49
+ }
50
+ export interface CompletePageVisitParts {
51
+ [PAGE_VISIT_VID_FRAG_ATT_NAME]: Array<VideoFrag>;
52
+ [PAGE_VISIT_INFORMATION_ATT_NAME]: PageVisitInfo;
53
+ [PAGE_VISIT_PART_ATT_NAME]: Array<PageVisitPart>;
54
+ }
@@ -1,4 +1,3 @@
1
- /// <reference types="react-native" />
2
1
  declare global {
3
2
  const METROPLEX_BASE_SOCKET_URL: string;
4
3
  const BETA_METROPLEX_BASE_SOCKET_URL: string;
@@ -15,6 +14,8 @@ declare global {
15
14
  const METROPLEX_CONSECUTIVE_CONNECTION_DELAY: number;
16
15
  const SCRIPT_ID: string;
17
16
  const ENABLE_LOGGING: string;
17
+ const ENV: string;
18
+ const DEVICE_ENV: string | undefined;
18
19
  type Window = {
19
20
  noibuJSLoaded?: boolean;
20
21
  };
@@ -0,0 +1,6 @@
1
+ /** Checks to see if the necessary Date functions that we use have been overwritten */
2
+ export function isDateOverwritten(): boolean;
3
+ /** Timestamp wrapper to properly handle timestamps
4
+ * @param {} timestamp
5
+ */
6
+ export function timestampWrapper(timestamp: any): any;
@@ -0,0 +1,8 @@
1
+ /** addSafeEventListener will add an event listener for the specified event
2
+ * but will catch and log any errors encountered
3
+ * @param {} object to attach the listener to
4
+ * @param {} event to listen to
5
+ * @param {} callback function to call
6
+ * @param {} [capture] additional arguments
7
+ */
8
+ export function addSafeEventListener(object: any, event: any, callback: any, capture?: any): void;
@@ -6,9 +6,9 @@ import ClientConfig from '../api/clientConfig.js';
6
6
  * @param {} object to attach the listener to
7
7
  * @param {} event to listen to
8
8
  * @param {} callback function to call
9
- * @param {} capture additional arguments
9
+ * @param {} [capture] additional arguments
10
10
  */
11
- function addSafeEventListener(object, event, callback, capture) {
11
+ function addSafeEventListener(object, event, callback, capture = false) {
12
12
  if (!object || !event || !callback) {
13
13
  // nothing to do if these don't exist
14
14
  return;
@@ -48,9 +48,8 @@ export declare function makeRequest(method: string, url: string, data: unknown,
48
48
  */
49
49
  export declare function isValidURL(str: string): boolean;
50
50
  /**
51
- * Because of the nature of user agent in react native, we have to make this async.
52
- * But I promise, this is really fast, since we memoize the result for the whole session :)
53
- * @returns {Promise<string>}
51
+ * Fakes the user agent retrieval, since there are no good libraries that support both expo and plain RN
52
+ * caches the result for the session
54
53
  */
55
54
  export declare function getUserAgent(): Promise<string>;
56
55
  /**
@@ -77,3 +76,7 @@ export declare function maskTextInput(text: string): string;
77
76
  * where type is not actually a type but an object.
78
77
  */
79
78
  export declare function isInstanceOf(instance: unknown, type: unknown): boolean;
79
+ /**
80
+ * To grab the video recorder type based on the device we run the app on.
81
+ */
82
+ export declare function getVideoRecorderType(): Promise<string>;
@@ -1,10 +1,9 @@
1
- import DeviceInfo from 'react-native-device-info';
1
+ import { Platform } from 'react-native';
2
2
  import { parseStack } from './stacktrace-parser.js';
3
3
  import { MAX_STRING_LENGTH, MAX_BEACON_PAYLOAD_SIZE, REQUIRED_DATA_PROCESSING_URLS, PII_EMAIL_PATTERN, PII_REDACTION_REPLACEMENT_STRING, PII_DIGIT_PATTERN, DEFAULT_STACK_FRAME_FIELD_VALUE } from '../constants.js';
4
4
  import { noibuLog } from './log.js';
5
5
  import { unwrapNoibuWrapped } from './object.js';
6
6
 
7
- /** @module Functions */
8
7
  /**
9
8
  * Returns a stack trace frame with default filed values
10
9
  */
@@ -188,20 +187,22 @@ function isValidURL(str) {
188
187
  }
189
188
  let userAgentCache = '';
190
189
  /**
191
- * Because of the nature of user agent in react native, we have to make this async.
192
- * But I promise, this is really fast, since we memoize the result for the whole session :)
193
- * @returns {Promise<string>}
190
+ * Fakes the user agent retrieval, since there are no good libraries that support both expo and plain RN
191
+ * caches the result for the session
194
192
  */
195
193
  async function getUserAgent() {
196
194
  if (userAgentCache) {
197
195
  return userAgentCache;
198
196
  }
199
- try {
200
- userAgentCache = await DeviceInfo.getUserAgent();
197
+ noibuLog('getUserAgent start');
198
+ if (Platform.OS === 'android') {
199
+ const { Brand, Model, Release } = Platform.constants;
200
+ userAgentCache = `Mozilla/5.0 (Linux; Android ${Release}; ${Brand} ${Model}; React Native ${Platform.Version}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36`;
201
201
  }
202
- catch (e) {
203
- userAgentCache = '';
202
+ else if (Platform.OS === 'ios') {
203
+ userAgentCache = `Mozilla/5.0 (iPhone; CPU iPhone OS ${Platform.constants.osVersion} like Mac OS X; React Native ${Platform.Version}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1`;
204
204
  }
205
+ noibuLog('getUserAgent end', { userAgentCache });
205
206
  return userAgentCache;
206
207
  }
207
208
  /**
@@ -268,5 +269,17 @@ function isInstanceOf(instance, type) {
268
269
  return false;
269
270
  }
270
271
  }
272
+ /**
273
+ * To grab the video recorder type based on the device we run the app on.
274
+ */
275
+ async function getVideoRecorderType() {
276
+ if (Platform.OS === 'android') {
277
+ return 'AndroidNative';
278
+ }
279
+ if (Platform.OS === 'ios') {
280
+ return 'IOSNative';
281
+ }
282
+ return '';
283
+ }
271
284
 
272
- export { asString, getJSStack, getMaxSubstringAllowed, getUserAgent, isInstanceOf, isInvalidURLConfig, isNoibuJSAlreadyLoaded, isStackTrace, isValidURL, makeRequest, maskTextInput, processFrames, stringifyJSON };
285
+ export { asString, getJSStack, getMaxSubstringAllowed, getUserAgent, getVideoRecorderType, isInstanceOf, isInvalidURLConfig, isNoibuJSAlreadyLoaded, isStackTrace, isValidURL, makeRequest, maskTextInput, processFrames, stringifyJSON };
@@ -1,4 +1,3 @@
1
- /// <reference types="react-native" />
2
1
  /**
3
2
  * log with level = info
4
3
  */
@@ -26,7 +26,7 @@ export declare function unwrapNoibuWrapped<T>(anything: {
26
26
  * @param {string} property
27
27
  * @returns {boolean} Whether the property on the prototype is (or is now) writeable
28
28
  */
29
- export declare const propWriteableOrMadeWriteable: (proto: object, property: never) => boolean;
29
+ export declare const propWriteableOrMadeWriteable: (proto: object, property: keyof typeof proto) => boolean;
30
30
  /**
31
31
  * Iterates object recursively and calls visit function
32
32
  * for each property allowing to override its value
@@ -35,7 +35,7 @@ export declare const propWriteableOrMadeWriteable: (proto: object, property: nev
35
35
  * There are 3 arguments: current object, current property and its value
36
36
  * @param {{depth: number}} limit Use limit config object to set depth of the recursion
37
37
  */
38
- export declare const iterateObjectRecursively: (instance: Record<any, any>, visit: (i: Record<any, any>, p: any, v: any) => any, limit?: {
38
+ export declare const iterateObjectRecursively: (instance: Record<any, any>, visit: (i: typeof instance, p: keyof typeof i, v: (typeof i)[typeof p]) => typeof v, limit?: {
39
39
  depth: number;
40
40
  }) => void;
41
41
  export {};
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "noibu-react-native",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "targetNjsVersion": "1.0.104",
5
5
  "description": "React-Native SDK for NoibuJS to collect errors in React-Native applications",
6
6
  "main": "dist/entry/index.js",
7
7
  "types": "dist/entry/index.d.ts",
8
8
  "files": [
9
+ "android/*",
9
10
  "dist/*",
10
11
  "README.md"
11
12
  ],
@@ -21,14 +22,16 @@
21
22
  "lint_output": "eslint src -c .eslintrc.json --ext js,ts,jsx,tsx -f json > eslint_report.json",
22
23
  "codecov": "codecov"
23
24
  },
24
- "optionalDependencies": {
25
- "react-native-navigation": "7"
25
+ "peerDependenciesMeta": {
26
+ "react-native-navigation": {
27
+ "optional": true
28
+ }
26
29
  },
27
30
  "dependencies": {
28
31
  "@react-native-async-storage/async-storage": "^1.19.0",
32
+ "fflate": "^0.8.2",
29
33
  "react": ">=16.11.0",
30
34
  "react-native": ">=0.63.0",
31
- "react-native-device-info": "^10.6.0",
32
35
  "react-native-url-polyfill": "^1.3.0",
33
36
  "react-native-uuid": "^2.0.1"
34
37
  },
@@ -40,10 +43,12 @@
40
43
  "@rollup/plugin-typescript": "^11.1.1",
41
44
  "@tsconfig/react-native": "^3.0.2",
42
45
  "@types/jest": "^29.5.1",
46
+ "@jest/globals": "^29.7.0",
43
47
  "@types/node": "^20.2.3",
44
48
  "@types/react": "^18.2.6",
45
49
  "@types/react-test-renderer": "^18.0.0",
46
50
  "@typescript-eslint/eslint-plugin": "^5.59.6",
51
+ "babel-plugin-transform-flow-strip-types": "^6.22.0",
47
52
  "codecov": "^3.8.3",
48
53
  "dotenv": "^16.1.3",
49
54
  "eslint": "^8.41.0",
@@ -51,10 +56,14 @@
51
56
  "eslint-config-prettier": "^8.8.0",
52
57
  "eslint-plugin-jsdoc": "^44.2.4",
53
58
  "eslint-plugin-prettier": "^4.2.1",
54
- "jest": "^29.5.0",
59
+ "jest": "^29.7.0",
60
+ "@babel/preset-env": "^7.24.8",
61
+ "babel-jest": "^29.7.0",
55
62
  "prettier": "^2.8.8",
56
63
  "rimraf": "^5.0.1",
57
64
  "rollup": "^3.24.0",
58
- "rollup-plugin-dotenv": "^0.5.0"
65
+ "rollup-plugin-dotenv": "^0.5.0",
66
+ "ts-jest": "^29.2.3",
67
+ "typescript": "^5.5.3"
59
68
  }
60
69
  }