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.
- 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 +2 -2
- package/dist/api/clientConfig.js +1 -1
- package/dist/api/helpCode.d.ts +29 -0
- package/dist/api/inputManager.d.ts +87 -0
- package/dist/api/metroplexSocket.d.ts +7 -6
- package/dist/api/metroplexSocket.js +14 -18
- 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 +6 -1
- package/dist/constants.js +13 -2
- package/dist/entry/index.d.ts +1 -1
- package/dist/entry/init.js +8 -4
- 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/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 +4 -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/StoredPageVisit.types.d.ts +54 -0
- package/dist/types/globals.d.ts +0 -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 +4 -0
- package/dist/utils/function.js +13 -1
- package/dist/utils/log.d.ts +0 -1
- package/dist/utils/log.js +3 -5
- package/dist/utils/object.d.ts +2 -2
- 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
|
+
}
|
package/dist/types/globals.d.ts
CHANGED
|
@@ -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
|
@@ -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>;
|