noibu-react-native 0.1.3 → 0.2.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 (48) 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 +2 -2
  8. package/dist/api/clientConfig.js +1 -1
  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 +7 -6
  12. package/dist/api/metroplexSocket.js +14 -18
  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 +6 -1
  17. package/dist/constants.js +13 -2
  18. package/dist/entry/index.d.ts +1 -1
  19. package/dist/entry/init.js +8 -4
  20. package/dist/monitors/clickMonitor.d.ts +44 -0
  21. package/dist/monitors/gqlErrorValidator.d.ts +82 -0
  22. package/dist/monitors/httpDataBundler.d.ts +161 -0
  23. package/dist/monitors/inputMonitor.d.ts +34 -0
  24. package/dist/monitors/keyboardInputMonitor.d.ts +17 -0
  25. package/dist/monitors/pageMonitor.d.ts +22 -0
  26. package/dist/monitors/requestMonitor.d.ts +10 -0
  27. package/dist/pageVisit/pageVisit.d.ts +52 -0
  28. package/dist/pageVisit/pageVisit.js +4 -2
  29. package/dist/pageVisit/pageVisitEventError.d.ts +15 -0
  30. package/dist/pageVisit/pageVisitEventHTTP.d.ts +18 -0
  31. package/dist/pageVisit/userStep.d.ts +5 -0
  32. package/dist/react/ErrorBoundary.js +17 -9
  33. package/dist/sessionRecorder/nativeSessionRecorderSubscription.d.ts +64 -0
  34. package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +58 -0
  35. package/dist/sessionRecorder/sessionRecorder.d.ts +60 -0
  36. package/dist/sessionRecorder/sessionRecorder.js +287 -0
  37. package/dist/sessionRecorder/types.d.ts +91 -0
  38. package/dist/types/StoredPageVisit.types.d.ts +54 -0
  39. package/dist/types/globals.d.ts +0 -1
  40. package/dist/utils/date.d.ts +6 -0
  41. package/dist/utils/eventlistener.d.ts +8 -0
  42. package/dist/utils/eventlistener.js +2 -2
  43. package/dist/utils/function.d.ts +4 -0
  44. package/dist/utils/function.js +13 -1
  45. package/dist/utils/log.d.ts +0 -1
  46. package/dist/utils/log.js +3 -5
  47. package/dist/utils/object.d.ts +2 -2
  48. package/package.json +11 -3
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The level of logging to show in the device logcat stream.
3
+ */
4
+ export declare enum LogLevel {
5
+ Verbose = "Verbose",
6
+ Debug = "Debug",
7
+ Info = "Info",
8
+ Warning = "Warning",
9
+ Error = "Error",
10
+ None = "None"
11
+ }
12
+ /**
13
+ * The configuration that will be used to customize the session recording behaviour.
14
+ *
15
+ * @param userId [OPTIONAL default = null] A custom identifier for the current user. If passed as null, the user id
16
+ * will be auto generated. The user id in general is sticky across sessions.
17
+ * The provided user id must follow these conditions:
18
+ * 1. Cannot be an empty string.
19
+ * 2. Should be base36 and smaller than "1Z141Z4".
20
+ * @param logLevel [OPTIONAL default = LogLevel.None] The level of logging to show in the device logcat stream.
21
+ * @param allowMeteredNetworkUsage [OPTIONAL default = false] Allows uploading session data to the servers on device metered network.
22
+ * @param enableWebViewCapture [OPTIONAL default = true] Allows Noibu - Session recorder to capture the web views DOM content.
23
+ * @param allowedDomains [OPTIONAL default = ["*"]] The whitelisted domains to allow Noibu - Session recorder to capture their DOM content.
24
+ * If it contains "*" as an element, all domains will be captured.
25
+ * @param disableOnLowEndDevices [OPTIONAL default = false] Disable Noibu - Session recorder on low-end devices.
26
+ * @param maximumDailyNetworkUsageInMB [OPTIONAL default = null] Maximum daily network usage for Noibu - Session recorder (null = No limit). When the limit is reached, Noibu - Session recorder will turn on lean mode.
27
+ */
28
+ export interface SessionRecorderConfig {
29
+ userId?: string | null;
30
+ logLevel?: LogLevel;
31
+ allowMeteredNetworkUsage?: boolean;
32
+ enableWebViewCapture?: boolean;
33
+ allowedDomains?: string[];
34
+ disableOnLowEndDevices?: boolean;
35
+ maximumDailyNetworkUsageInMB?: number;
36
+ }
37
+ /**
38
+ * Initializes the Noibu - Session recording SDK if the API level is supported.
39
+ * param projectId [REQUIRED] The session recording project id to send data to.
40
+ * param config [OPTIONAL] The sessionreplay config, if not provided default values are used.
41
+ */
42
+ export declare function initialize(projectId: string, config?: SessionRecorderConfig): void;
43
+ /**
44
+ * Sets a custom user id that can be used to identify the user. It has less
45
+ * restrictions than the userId parameter. You can pass any string and
46
+ * you can filter on it on the dashboard side. If you need the most efficient
47
+ * filtering on the dashboard, use the userId parameter if possible.
48
+ * <p>
49
+ * Note: custom user id cannot be null or empty, or consists only of whitespaces.
50
+ * </p>
51
+ * @param customUserId The custom user id to set.
52
+ */
53
+ export declare function setCustomUserId(customUserId: string): void;
54
+ /**
55
+ * Sets a custom session id that can be used to identify the session.
56
+ * <p>
57
+ * Note: custom session id cannot be null or empty, or consists only of whitespaces.
58
+ * </p>
59
+ * @param customSessionId The custom session id to set.
60
+ */
61
+ export declare function setCustomSessionId(customSessionId: string): void;
62
+ export type RecorderEvent = import('./types').RecorderEvent;
63
+ export type UnsubscribeFn = import('./types').UnsubscribeFn;
64
+ export declare function subscribeToNativeEvent(callback: (event: RecorderEvent) => void): UnsubscribeFn;
@@ -0,0 +1,58 @@
1
+ import { NativeEventEmitter, Platform, NativeModules } from 'react-native';
2
+ import { noibuLog } from '../utils/log.js';
3
+
4
+ const LINKING_ERROR = `The package 'noibu-session-replay' doesn't seem to be linked. Make sure: \n\n` +
5
+ // Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + TODO: add back when iOS is supported.
6
+ '- You rebuilt the app after installing the package\n' +
7
+ '- You are not using Expo Go\n';
8
+ const { NativeSessionRecorder } = NativeModules;
9
+ let nativeModuleEmitter;
10
+ const SupportedPlatforms = ['android'];
11
+ /**
12
+ * The level of logging to show in the device logcat stream.
13
+ */
14
+ // eslint-disable-next-line no-shadow
15
+ var LogLevel;
16
+ (function (LogLevel) {
17
+ LogLevel["Verbose"] = "Verbose";
18
+ LogLevel["Debug"] = "Debug";
19
+ LogLevel["Info"] = "Info";
20
+ LogLevel["Warning"] = "Warning";
21
+ LogLevel["Error"] = "Error";
22
+ LogLevel["None"] = "None";
23
+ })(LogLevel || (LogLevel = {}));
24
+ /**
25
+ * Initializes the Noibu - Session recording SDK if the API level is supported.
26
+ * param projectId [REQUIRED] The session recording project id to send data to.
27
+ * param config [OPTIONAL] The sessionreplay config, if not provided default values are used.
28
+ */
29
+ function initialize(projectId, config) {
30
+ if (!(typeof config === 'object' || typeof config === 'undefined')) {
31
+ throw Error('Invalid session recording initialization arguments. Please check the docs for assitance.');
32
+ }
33
+ nativeModuleEmitter = new NativeEventEmitter(NativeSessionRecorder);
34
+ // applying default values
35
+ const { userId = null, logLevel = LogLevel.None, allowMeteredNetworkUsage = false, enableWebViewCapture = true, allowedDomains = ['*'], disableOnLowEndDevices = false, maximumDailyNetworkUsageInMB = null, } = config ?? {};
36
+ if (!SupportedPlatforms.includes(Platform.OS)) {
37
+ noibuLog(`Noibu - Session recording supports ${SupportedPlatforms.join(', ')} only for now.`);
38
+ return;
39
+ }
40
+ if (NativeSessionRecorder === null) {
41
+ noibuLog('Noibu - Session recording did not initialize properly.', LINKING_ERROR);
42
+ return;
43
+ }
44
+ // We use two parameters because the react method parameters do not accept nullable primitive types.
45
+ const enableDailyNetworkUsageLimit = maximumDailyNetworkUsageInMB != null;
46
+ const refinedMaximumDailyNetworkUsageInMB = maximumDailyNetworkUsageInMB ?? 0;
47
+ NativeSessionRecorder.initialize(projectId, userId, logLevel, allowMeteredNetworkUsage, enableWebViewCapture, allowedDomains, disableOnLowEndDevices, enableDailyNetworkUsageLimit, refinedMaximumDailyNetworkUsageInMB);
48
+ }
49
+ function subscribeToNativeEvent(callback) {
50
+ if (!nativeModuleEmitter) {
51
+ throw new Error('You have to initialize Noibu Session Recorder first');
52
+ }
53
+ nativeModuleEmitter.addListener('noibuRecordingEvent', callback);
54
+ // return () => subscription.remove();
55
+ return () => { };
56
+ }
57
+
58
+ export { LogLevel, initialize, subscribeToNativeEvent };
@@ -0,0 +1,60 @@
1
+ import { RecorderEvent } from './nativeSessionRecorderSubscription';
2
+ /** Singleton class to record user sessions */
3
+ export default class SessionRecorder {
4
+ private static instance;
5
+ private eventBuffer;
6
+ private vfCounter;
7
+ private didSetupRecorder;
8
+ private isVideoLengthNegativeInvalid;
9
+ private lastFragPostTimestamp;
10
+ private pauseTimeout;
11
+ private lastRecordedTimestamp;
12
+ private firstRecordedTimestamp;
13
+ private recordStopper;
14
+ private freezingEvents;
15
+ /**
16
+ * Creates an instance of the session recorder
17
+ */
18
+ constructor();
19
+ /**
20
+ * Setups the SessionRecorder instance for usage
21
+ */
22
+ static getInstance(): SessionRecorder;
23
+ /** Sets up the page hide handler to try to push remaining video events */
24
+ setupUnloadHandler(): void;
25
+ /** Sets up the post metrics handler to potentially log a debug message */
26
+ setupPostMetricsHandler(): void;
27
+ /**
28
+ * Starts recording the user session
29
+ */
30
+ recordUserSession(): Promise<void>;
31
+ /**
32
+ * handleNewRRwebEvent will process each upcoming.
33
+ * rrweb event. It will make sure that the current buffer
34
+ * is updated with the latest events and post the contents
35
+ * of the buffer if it exceeds max size
36
+ */
37
+ handleRecorderEvent(recorderEvent: RecorderEvent): Promise<void>;
38
+ /**
39
+ * Compress event
40
+ */
41
+ private pack;
42
+ private static compress;
43
+ /** builds a log message with debug info
44
+ */
45
+ buildDebugMessage(eventName: string, totalVideoTime: number, sessionLength: number): string;
46
+ /**
47
+ * handleFragPost communicates with the Metroplex socket
48
+ * to post video fragments when needed. It also handles
49
+ * necessary management of the buffer and it's related
50
+ * variables
51
+ */
52
+ handleFragPost(): Promise<void>;
53
+ /**
54
+ * unfreeze forcefully resumes recording events in case it was frozen
55
+ * waiting for user events
56
+ */
57
+ unfreeze(): Promise<void>;
58
+ /** stops recording */
59
+ private freeze;
60
+ }
@@ -0,0 +1,287 @@
1
+ import { strToU8, zlibSync, strFromU8 } from 'fflate';
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 {};
@@ -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;
@@ -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;
@@ -76,3 +76,7 @@ export declare function maskTextInput(text: string): string;
76
76
  * where type is not actually a type but an object.
77
77
  */
78
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>;