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.
- package/android/.gitignore +13 -0
- package/android/build.gradle +79 -0
- package/android/gradle.properties +7 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt +107 -0
- package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayPackage.kt +17 -0
- package/dist/api/clientConfig.d.ts +6 -7
- package/dist/api/clientConfig.js +14 -16
- package/dist/api/helpCode.d.ts +29 -0
- package/dist/api/inputManager.d.ts +87 -0
- package/dist/api/metroplexSocket.d.ts +156 -0
- package/dist/api/metroplexSocket.js +662 -815
- package/dist/api/storedMetrics.d.ts +73 -0
- package/dist/api/storedPageVisit.d.ts +49 -0
- package/dist/const_matchers.d.ts +1 -0
- package/dist/constants.d.ts +10 -1
- package/dist/constants.js +19 -2
- package/dist/entry/index.d.ts +1 -1
- package/dist/entry/init.d.ts +1 -1
- package/dist/entry/init.js +10 -6
- package/dist/monitors/appNavigationMonitor.js +3 -2
- package/dist/monitors/clickMonitor.d.ts +44 -0
- package/dist/monitors/gqlErrorValidator.d.ts +82 -0
- package/dist/monitors/httpDataBundler.d.ts +161 -0
- package/dist/monitors/inputMonitor.d.ts +34 -0
- package/dist/monitors/inputMonitor.js +5 -0
- package/dist/monitors/integrations/react-native-navigation-integration.d.ts +1 -2
- package/dist/monitors/keyboardInputMonitor.d.ts +17 -0
- package/dist/monitors/pageMonitor.d.ts +22 -0
- package/dist/monitors/requestMonitor.d.ts +10 -0
- package/dist/pageVisit/pageVisit.d.ts +52 -0
- package/dist/pageVisit/pageVisit.js +9 -2
- package/dist/pageVisit/pageVisitEventError.d.ts +15 -0
- package/dist/pageVisit/pageVisitEventHTTP.d.ts +18 -0
- package/dist/pageVisit/userStep.d.ts +5 -0
- package/dist/react/ErrorBoundary.js +17 -9
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.d.ts +64 -0
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +58 -0
- package/dist/sessionRecorder/sessionRecorder.d.ts +60 -0
- package/dist/sessionRecorder/sessionRecorder.js +287 -0
- package/dist/sessionRecorder/types.d.ts +91 -0
- package/dist/types/NavigationIntegration.d.ts +1 -2
- package/dist/types/StoredPageVisit.types.d.ts +54 -0
- package/dist/types/globals.d.ts +2 -1
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/eventlistener.d.ts +8 -0
- package/dist/utils/eventlistener.js +2 -2
- package/dist/utils/function.d.ts +6 -3
- package/dist/utils/function.js +23 -10
- package/dist/utils/log.d.ts +0 -1
- package/dist/utils/object.d.ts +2 -2
- 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:
|
|
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
|
+
}
|
package/dist/types/globals.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/function.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
52
|
-
*
|
|
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>;
|
package/dist/utils/function.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
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
|
-
*
|
|
192
|
-
*
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/utils/log.d.ts
CHANGED
package/dist/utils/object.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
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
|
-
"
|
|
25
|
-
"react-native-navigation":
|
|
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.
|
|
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
|
}
|