noibu-react-native 0.2.2 → 0.2.4
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/README.md +1 -1
- package/dist/api/clientConfig.js +225 -217
- package/dist/api/helpCode.js +61 -87
- package/dist/api/metroplexSocket.js +460 -463
- package/dist/api/storedPageVisit.js +150 -208
- package/dist/constants.js +10 -2
- package/dist/entry/init.js +65 -63
- package/dist/monitors/{appNavigationMonitor.js → AppNavigationMonitor.js} +12 -22
- package/dist/monitors/ClickMonitor.js +198 -0
- package/dist/monitors/ErrorMonitor.js +206 -0
- package/dist/monitors/KeyboardInputMonitor.js +60 -0
- package/dist/monitors/PageMonitor.js +98 -0
- package/dist/monitors/RequestMonitor.js +390 -0
- package/dist/monitors/http-tools/GqlErrorValidator.js +259 -0
- package/dist/monitors/http-tools/HTTPDataBundler.js +458 -0
- package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
- package/dist/pageVisit/EventDebouncer.js +99 -0
- package/dist/pageVisit/pageVisitEventError.js +2 -2
- package/dist/pageVisit/pageVisitEventHTTP.js +79 -93
- package/dist/react/ErrorBoundary.js +18 -15
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
- package/dist/sessionRecorder/sessionRecorder.js +152 -151
- package/dist/{api → src/api}/clientConfig.d.ts +2 -2
- package/dist/{api → src/api}/helpCode.d.ts +10 -16
- package/dist/{api → src/api}/metroplexSocket.d.ts +48 -67
- package/dist/{api → src/api}/storedPageVisit.d.ts +12 -21
- package/dist/{constants.d.ts → src/constants.d.ts} +45 -0
- package/dist/{entry → src/entry}/init.d.ts +1 -1
- package/dist/src/monitors/AppNavigationMonitor.d.ts +18 -0
- package/dist/src/monitors/ClickMonitor.d.ts +31 -0
- package/dist/src/monitors/ErrorMonitor.d.ts +63 -0
- package/dist/{monitors/keyboardInputMonitor.d.ts → src/monitors/KeyboardInputMonitor.d.ts} +7 -4
- package/dist/{monitors/pageMonitor.d.ts → src/monitors/PageMonitor.d.ts} +6 -8
- package/dist/src/monitors/RequestMonitor.d.ts +94 -0
- package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +59 -0
- package/dist/src/monitors/http-tools/HTTPDataBundler.d.ts +112 -0
- package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +3 -2
- package/dist/src/pageVisit/EventDebouncer.d.ts +24 -0
- package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +1 -1
- package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
- package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
- package/dist/{storage → src/storage}/rnStorageProvider.d.ts +1 -1
- package/dist/{storage → src/storage}/storage.d.ts +2 -2
- package/dist/{storage → src/storage}/storageProvider.d.ts +3 -3
- package/dist/{utils → src/utils}/function.d.ts +27 -7
- package/dist/{utils → src/utils}/object.d.ts +11 -8
- package/dist/src/utils/piiRedactor.d.ts +11 -0
- package/dist/src/utils/polyfills.d.ts +4 -0
- package/dist/storage/rnStorageProvider.js +7 -4
- package/dist/storage/storage.js +43 -35
- package/dist/storage/storageProvider.js +23 -19
- package/dist/types/Config.d.ts +24 -20
- package/dist/types/Metroplex.types.d.ts +73 -0
- package/dist/types/Monitor.d.ts +11 -0
- package/dist/types/Monitor.js +19 -0
- package/dist/types/PageVisit.types.d.ts +8 -0
- package/dist/types/PageVisitErrors.types.d.ts +114 -0
- package/dist/types/PageVisitEvents.types.d.ts +91 -0
- package/dist/types/PageVisitMetrics.types.d.ts +27 -0
- package/dist/types/Storage.d.ts +1 -1
- package/dist/types/StoredPageVisit.types.d.ts +4 -47
- package/dist/types/WrappedObjects.d.ts +6 -0
- package/dist/utils/function.js +110 -77
- package/dist/utils/object.js +59 -6
- package/dist/utils/piiRedactor.js +98 -0
- package/dist/utils/polyfills.js +24 -0
- package/package.json +8 -8
- package/dist/monitors/appNavigationMonitor.d.ts +0 -22
- package/dist/monitors/clickMonitor.d.ts +0 -44
- package/dist/monitors/clickMonitor.js +0 -251
- package/dist/monitors/errorMonitor.d.ts +0 -28
- package/dist/monitors/errorMonitor.js +0 -180
- package/dist/monitors/gqlErrorValidator.d.ts +0 -82
- package/dist/monitors/gqlErrorValidator.js +0 -306
- package/dist/monitors/httpDataBundler.d.ts +0 -161
- package/dist/monitors/httpDataBundler.js +0 -725
- package/dist/monitors/inputMonitor.d.ts +0 -34
- package/dist/monitors/inputMonitor.js +0 -138
- package/dist/monitors/keyboardInputMonitor.js +0 -66
- package/dist/monitors/pageMonitor.js +0 -122
- package/dist/monitors/requestMonitor.d.ts +0 -10
- package/dist/monitors/requestMonitor.js +0 -401
- package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
- package/dist/types/PageVisit.d.ts +0 -22
- package/dist/types/ReactNative.d.ts +0 -4
- package/dist/types/globals.d.ts +0 -45
- /package/dist/{api → src/api}/inputManager.d.ts +0 -0
- /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
- /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
- /package/dist/{entry → src/entry}/index.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
- /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
- /package/dist/{utils → src/utils}/date.d.ts +0 -0
- /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
- /package/dist/{utils → src/utils}/log.d.ts +0 -0
- /package/dist/{utils → src/utils}/performance.d.ts +0 -0
- /package/dist/{utils → src/utils}/stacktrace-parser.d.ts +0 -0
|
@@ -1,112 +1,98 @@
|
|
|
1
|
-
import { getMaxSubstringAllowed, asString } from '../utils/function.js';
|
|
2
1
|
import { timestampWrapper } from '../utils/date.js';
|
|
3
|
-
import {
|
|
4
|
-
import { HTTP_METHOD_ATT_NAME, MAX_HTTP_DATA_EVENT_COUNT, PV_SEQ_ATT_NAME, HTTP_DATA_METROPLEX_TYPE, HTTP_EVENT_TYPE, PAGE_VISIT_HTTP_DATA_ATT_NAME } from '../constants.js';
|
|
2
|
+
import { HTTP_RESP_CODE_ATT_NAME, HTTP_RESP_TIME_ATT_NAME, HTTP_METHOD_ATT_NAME, URL_ATT_NAME, PV_SEQ_ATT_NAME, PAGE_VISIT_HTTP_DATA_ATT_NAME, HTTP_DATA_METROPLEX_TYPE, HTTP_EVENT_TYPE, MAX_HTTP_DATA_EVENT_COUNT, MAX_HTTP_DATA_IF_ERROR_EVENT_COUNT } from '../constants.js';
|
|
5
3
|
import { PageVisit } from './pageVisit.js';
|
|
6
4
|
import StoredMetrics from '../api/storedMetrics.js';
|
|
7
5
|
import MetroplexSocket from '../api/metroplexSocket.js';
|
|
8
|
-
import {
|
|
6
|
+
import { getMaxSubstringAllowed, asString, safeTrim } from '../utils/function.js';
|
|
7
|
+
import { EventDebouncer } from './EventDebouncer.js';
|
|
9
8
|
|
|
10
9
|
/** @module PageVisitEventHTTP */
|
|
11
|
-
|
|
12
10
|
/**
|
|
13
11
|
* Determines if a response is a failure
|
|
14
|
-
* @param {number} code
|
|
15
12
|
*/
|
|
16
13
|
function isHttpCodeFailure(code) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return code >= 400 || code <= 0;
|
|
14
|
+
if (typeof code !== 'number') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return code >= 400 || code <= 0;
|
|
22
18
|
}
|
|
23
|
-
|
|
24
19
|
/** Class representing a PageVisitEventHTTP */
|
|
25
20
|
class PageVisitEventHTTP {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
mutatedHttpEvent[HTTP_METHOD_ATT_NAME] =
|
|
41
|
-
httpEvent[HTTP_METHOD_ATT_NAME].toUpperCase();
|
|
42
|
-
|
|
43
|
-
mutatedHttpEvent.url = getMaxSubstringAllowed(
|
|
44
|
-
asString(mutatedHttpEvent.url),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
this.httpEvent = mutatedHttpEvent;
|
|
48
|
-
this.httpData = httpData;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Saves the HTTP event to the pageVisit Queue */
|
|
52
|
-
saveHTTPEvent() {
|
|
53
|
-
noibuLog('saveHTTPEvent');
|
|
54
|
-
// we do not store http events that have empty urls
|
|
55
|
-
if (
|
|
56
|
-
!this.httpEvent ||
|
|
57
|
-
!this.httpEvent.url ||
|
|
58
|
-
this.httpEvent.url.trim() === ''
|
|
59
|
-
) {
|
|
60
|
-
noibuLog('saveHTTPEvent dropped due to empty url');
|
|
61
|
-
return;
|
|
21
|
+
/**
|
|
22
|
+
* Creates an instance of the http event for the pv
|
|
23
|
+
*/
|
|
24
|
+
constructor(httpEvent, httpData, isGqlError = false) {
|
|
25
|
+
/** if no value or it's less than 0, fallback to 0 */
|
|
26
|
+
const validate = (value) => (!value || value < 0 ? 0 : value);
|
|
27
|
+
this.httpEvent = {
|
|
28
|
+
[HTTP_RESP_CODE_ATT_NAME]: validate(httpEvent[HTTP_RESP_CODE_ATT_NAME]),
|
|
29
|
+
[HTTP_RESP_TIME_ATT_NAME]: validate(httpEvent[HTTP_RESP_TIME_ATT_NAME]),
|
|
30
|
+
[HTTP_METHOD_ATT_NAME]: (httpEvent[HTTP_METHOD_ATT_NAME] || 'get').toUpperCase(),
|
|
31
|
+
[URL_ATT_NAME]: getMaxSubstringAllowed(asString(httpEvent[URL_ATT_NAME])),
|
|
32
|
+
};
|
|
33
|
+
this.httpData = httpData;
|
|
34
|
+
this.isGqlError = isGqlError;
|
|
62
35
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
36
|
+
/** Saves the HTTP event to the pageVisit Queue */
|
|
37
|
+
saveHTTPEvent() {
|
|
38
|
+
// we do not store http events that have empty urls
|
|
39
|
+
if (!this.httpEvent || !safeTrim(this.httpEvent[URL_ATT_NAME])) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// we register an http event
|
|
43
|
+
StoredMetrics.getInstance().addHttpEvent();
|
|
44
|
+
const status = this.httpEvent[HTTP_RESP_CODE_ATT_NAME];
|
|
45
|
+
// send http data down to metroplex
|
|
46
|
+
if (this.httpData) {
|
|
47
|
+
// add the sequence number to both events
|
|
48
|
+
const sequenceNumber = StoredMetrics.getInstance().httpSequenceNumber;
|
|
49
|
+
const isSendAllowed = PageVisitEventHTTP.isSendAllowed(status, sequenceNumber, this.isGqlError);
|
|
50
|
+
// restrict total number of events collected per page visit to ensure we don't
|
|
51
|
+
// blow up memory and storage usage
|
|
52
|
+
if (isSendAllowed) {
|
|
53
|
+
this.httpData[PV_SEQ_ATT_NAME] = sequenceNumber;
|
|
54
|
+
this.httpEvent[PV_SEQ_ATT_NAME] = sequenceNumber;
|
|
55
|
+
// increment the count
|
|
56
|
+
StoredMetrics.getInstance().addHttpData();
|
|
57
|
+
const metroplexMsg = {
|
|
58
|
+
[PAGE_VISIT_HTTP_DATA_ATT_NAME]: this.httpData,
|
|
59
|
+
};
|
|
60
|
+
MetroplexSocket.getInstance().sendMessage(HTTP_DATA_METROPLEX_TYPE, metroplexMsg);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// have collected more than the max number of http requests for this
|
|
64
|
+
// page visit, so increment the over request limit count
|
|
65
|
+
StoredMetrics.getInstance().addHttpDataOverLimit();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// if this was an error, send immediately so we don't lose it
|
|
69
|
+
if (isHttpCodeFailure(status)) {
|
|
70
|
+
PageVisit.getInstance().addPageVisitEvent({
|
|
71
|
+
event: this.httpEvent,
|
|
72
|
+
occurredAt: new Date(timestampWrapper(Date.now())).toISOString(),
|
|
73
|
+
}, HTTP_EVENT_TYPE);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// debounce event
|
|
77
|
+
EventDebouncer.getInstance().addEvent(this.httpEvent, HTTP_EVENT_TYPE);
|
|
93
78
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Checks if sending data is allowed based on the HTTP status code and count.
|
|
81
|
+
* status - The HTTP status code to evaluate.
|
|
82
|
+
* count - The count of events to consider.
|
|
83
|
+
* isGqlError - Whether the context is considered as a GQL error.
|
|
84
|
+
* Returns `true` if sending data is allowed, `false` otherwise.
|
|
85
|
+
*/
|
|
86
|
+
static isSendAllowed(status, count, isGqlError = false) {
|
|
87
|
+
const isFailure = isHttpCodeFailure(status) || isGqlError;
|
|
88
|
+
const isSuccess = !isFailure;
|
|
89
|
+
if (isSuccess) {
|
|
90
|
+
if (count < MAX_HTTP_DATA_EVENT_COUNT) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return isFailure && count < MAX_HTTP_DATA_IF_ERROR_EVENT_COUNT;
|
|
105
95
|
}
|
|
106
|
-
|
|
107
|
-
// debounce event
|
|
108
|
-
InputMonitor.getInstance().addEvent(this.httpEvent, HTTP_EVENT_TYPE);
|
|
109
|
-
}
|
|
110
96
|
}
|
|
111
97
|
|
|
112
98
|
export { PageVisitEventHTTP, isHttpCodeFailure };
|
|
@@ -13,7 +13,24 @@ const INITIAL_STATE = {
|
|
|
13
13
|
* @extends {Component<ErrorBoundaryProps, ErrorBoundaryState>}
|
|
14
14
|
*/
|
|
15
15
|
class ErrorBoundary extends React.Component {
|
|
16
|
-
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments);
|
|
18
|
+
this.state = INITIAL_STATE;
|
|
19
|
+
/**
|
|
20
|
+
* Callback from fallback to reset the error boundary
|
|
21
|
+
* @param {} =>void=(
|
|
22
|
+
*/
|
|
23
|
+
this.resetErrorBoundary = () => {
|
|
24
|
+
const { onReset } = this.props;
|
|
25
|
+
const { error, componentStack, eventId } = this.state;
|
|
26
|
+
if (onReset) {
|
|
27
|
+
if (typeof componentStack === 'string') {
|
|
28
|
+
onReset(error, componentStack, eventId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.setState(INITIAL_STATE);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
17
34
|
/**
|
|
18
35
|
* Lifecycle hook on mount
|
|
19
36
|
* @returns void
|
|
@@ -57,20 +74,6 @@ class ErrorBoundary extends React.Component {
|
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Callback from fallback to reset the error boundary
|
|
62
|
-
* @param {} =>void=(
|
|
63
|
-
*/
|
|
64
|
-
resetErrorBoundary = () => {
|
|
65
|
-
const { onReset } = this.props;
|
|
66
|
-
const { error, componentStack, eventId } = this.state;
|
|
67
|
-
if (onReset) {
|
|
68
|
-
if (typeof componentStack === 'string') {
|
|
69
|
-
onReset(error, componentStack, eventId);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
this.setState(INITIAL_STATE);
|
|
73
|
-
};
|
|
74
77
|
/**
|
|
75
78
|
*
|
|
76
79
|
* Renders the fallback ui
|
|
@@ -32,7 +32,7 @@ function initialize(projectId, config) {
|
|
|
32
32
|
}
|
|
33
33
|
nativeModuleEmitter = new NativeEventEmitter(NativeSessionRecorder);
|
|
34
34
|
// applying default values
|
|
35
|
-
const { userId = null, logLevel = LogLevel.None, allowMeteredNetworkUsage = false, enableWebViewCapture = true, allowedDomains = ['*'], disableOnLowEndDevices = false, maximumDailyNetworkUsageInMB = null, } = config
|
|
35
|
+
const { userId = null, logLevel = LogLevel.None, allowMeteredNetworkUsage = false, enableWebViewCapture = true, allowedDomains = ['*'], disableOnLowEndDevices = false, maximumDailyNetworkUsageInMB = null, } = config !== null && config !== void 0 ? config : {};
|
|
36
36
|
if (!SupportedPlatforms.includes(Platform.OS)) {
|
|
37
37
|
noibuLog(`Noibu - Session recording supports ${SupportedPlatforms.join(', ')} only for now.`);
|
|
38
38
|
return;
|
|
@@ -43,13 +43,14 @@ function initialize(projectId, config) {
|
|
|
43
43
|
}
|
|
44
44
|
// We use two parameters because the react method parameters do not accept nullable primitive types.
|
|
45
45
|
const enableDailyNetworkUsageLimit = maximumDailyNetworkUsageInMB != null;
|
|
46
|
-
const refinedMaximumDailyNetworkUsageInMB = maximumDailyNetworkUsageInMB
|
|
46
|
+
const refinedMaximumDailyNetworkUsageInMB = maximumDailyNetworkUsageInMB !== null && maximumDailyNetworkUsageInMB !== void 0 ? maximumDailyNetworkUsageInMB : 0;
|
|
47
47
|
NativeSessionRecorder.initialize(projectId, userId, logLevel, allowMeteredNetworkUsage, enableWebViewCapture, allowedDomains, disableOnLowEndDevices, enableDailyNetworkUsageLimit, refinedMaximumDailyNetworkUsageInMB);
|
|
48
48
|
}
|
|
49
49
|
function subscribeToNativeEvent(callback) {
|
|
50
50
|
if (!nativeModuleEmitter) {
|
|
51
51
|
throw new Error('You have to initialize Noibu Session Recorder first');
|
|
52
52
|
}
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
53
54
|
nativeModuleEmitter.addListener('noibuRecordingEvent', callback);
|
|
54
55
|
// return () => subscription.remove();
|
|
55
56
|
return () => { };
|
|
@@ -1,30 +1,21 @@
|
|
|
1
|
+
import { __awaiter } from 'tslib';
|
|
1
2
|
import { strToU8, zlibSync, strFromU8 } from 'fflate';
|
|
2
3
|
import { LogLevel, initialize, subscribeToNativeEvent } from './nativeSessionRecorderSubscription.js';
|
|
3
4
|
import StoredMetrics from '../api/storedMetrics.js';
|
|
4
5
|
import ClientConfig from '../api/clientConfig.js';
|
|
5
6
|
import { stringifyJSON } from '../utils/function.js';
|
|
6
|
-
import {
|
|
7
|
+
import { MAX_TIME_FOR_RECORDER_USER_EVENTS, SEVERITY, 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, POST_METRICS_EVENT_NAME } from '../constants.js';
|
|
7
8
|
import MetroplexSocket from '../api/metroplexSocket.js';
|
|
8
9
|
import { addSafeEventListener } from '../utils/eventlistener.js';
|
|
9
10
|
import { noibuLog } from '../utils/log.js';
|
|
10
11
|
|
|
11
12
|
/** Singleton class to record user sessions */
|
|
12
13
|
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
14
|
/**
|
|
25
15
|
* Creates an instance of the session recorder
|
|
26
16
|
*/
|
|
27
17
|
constructor() {
|
|
18
|
+
this.freezingEvents = false;
|
|
28
19
|
this.eventBuffer = [];
|
|
29
20
|
this.vfCounter = 0;
|
|
30
21
|
this.didSetupRecorder = false;
|
|
@@ -94,21 +85,23 @@ class SessionRecorder {
|
|
|
94
85
|
/**
|
|
95
86
|
* Starts recording the user session
|
|
96
87
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
recordUserSession() {
|
|
89
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
// check if inactive before starting any recording
|
|
91
|
+
noibuLog('recordUserSession');
|
|
92
|
+
if ((yield MetroplexSocket.getInstance().closeIfInactive()) ||
|
|
93
|
+
StoredMetrics.getInstance().didCutVideo) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// making sure we are not attempting to call this method
|
|
97
|
+
// multiple times.
|
|
98
|
+
if (this.didSetupRecorder) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
StoredMetrics.getInstance().setDidStartVideo();
|
|
102
|
+
this.recordStopper = subscribeToNativeEvent(this.handleRecorderEvent.bind(this));
|
|
103
|
+
this.didSetupRecorder = true;
|
|
104
|
+
});
|
|
112
105
|
}
|
|
113
106
|
/**
|
|
114
107
|
* handleNewRRwebEvent will process each upcoming.
|
|
@@ -116,71 +109,75 @@ class SessionRecorder {
|
|
|
116
109
|
* is updated with the latest events and post the contents
|
|
117
110
|
* of the buffer if it exceeds max size
|
|
118
111
|
*/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.firstRecordedTimestamp
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.lastRecordedTimestamp
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
112
|
+
handleRecorderEvent(recorderEvent) {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const timestamp = Date.now();
|
|
115
|
+
// check if inactive before any processing
|
|
116
|
+
if ((yield MetroplexSocket.getInstance().closeIfInactive()) ||
|
|
117
|
+
StoredMetrics.getInstance().didCutVideo) {
|
|
118
|
+
this.freeze();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// determine if timeout should be extended based on event/source type
|
|
122
|
+
// event type 3 is an incremental snapshot
|
|
123
|
+
// event data source 0 is a mutation (all other data sources are user events)
|
|
124
|
+
if (this.pauseTimeout) {
|
|
125
|
+
// received a user event, extend the timeout
|
|
126
|
+
clearTimeout(this.pauseTimeout);
|
|
127
|
+
this.freezingEvents = false;
|
|
128
|
+
}
|
|
129
|
+
this.pauseTimeout = setTimeout(() => {
|
|
130
|
+
// stop recording page mutations after 2s of inactivity
|
|
131
|
+
// otherwise sites with many mutations will hit max video size
|
|
132
|
+
// in a short amount of time without any user events
|
|
133
|
+
this.freezingEvents = true;
|
|
134
|
+
// freezePage stops emitting events until the next user event is received
|
|
135
|
+
this.freeze();
|
|
136
|
+
}, MAX_TIME_FOR_RECORDER_USER_EVENTS);
|
|
137
|
+
// Set the first recorded timestamp if it hasn't been set yet.
|
|
138
|
+
// We usually only want this to be set once as the first recorded timestamp
|
|
139
|
+
// should not change.
|
|
140
|
+
if (!this.firstRecordedTimestamp) {
|
|
141
|
+
this.firstRecordedTimestamp = timestamp;
|
|
142
|
+
}
|
|
143
|
+
// Set the last recorded timestamp if it hasn't been set yet or a newly received rrweb
|
|
144
|
+
// event has a more recent timestamp than the last recorded timestamp.
|
|
145
|
+
if (!this.lastRecordedTimestamp || timestamp > this.lastRecordedTimestamp) {
|
|
146
|
+
this.lastRecordedTimestamp = timestamp;
|
|
147
|
+
}
|
|
148
|
+
// Checks if we've gone back in time for some reason.
|
|
149
|
+
// If we have, adjust our data accordingly to ensure we don't mess up
|
|
150
|
+
// the metrics 'exp_vid_len' data.
|
|
151
|
+
// If we don't adjust for time, we assume that the expected video length is
|
|
152
|
+
// the difference between the first recorded timestamp and the last recorded timestamp.
|
|
153
|
+
if (this.firstRecordedTimestamp &&
|
|
154
|
+
timestamp < this.firstRecordedTimestamp) {
|
|
155
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Detected time rewind. Client has been disabled.`, true, SEVERITY.error, true);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const packedEvent = yield this.pack(recorderEvent.message);
|
|
159
|
+
// Buffer the event for sending to metroplex
|
|
160
|
+
this.eventBuffer.push(packedEvent);
|
|
161
|
+
// Check if the event was a click or a double click. This is true if the root type is
|
|
162
|
+
// incremental snapshot (3) and the data source is mouse interaction data (2).
|
|
163
|
+
// Finally, we capture a click (2) or double click (4) event.
|
|
164
|
+
// todo if there are clicks, call StoredMetrics.getInstance().addVideoClick(); for each click
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const delta = now - this.lastFragPostTimestamp;
|
|
167
|
+
if (this.eventBuffer.length >= MAX_RECORDER_EVENT_BUFFER ||
|
|
168
|
+
delta > MAX_TIME_FOR_UNSENT_DATA_MILLIS) {
|
|
169
|
+
this.handleFragPost();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
177
172
|
}
|
|
178
173
|
/**
|
|
179
174
|
* Compress event
|
|
180
175
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
pack(recorderEvent) {
|
|
177
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
178
|
+
// return JSON.stringify(recorderEvent);
|
|
179
|
+
return SessionRecorder.compress(recorderEvent);
|
|
180
|
+
});
|
|
184
181
|
}
|
|
185
182
|
static compress(snapshot) {
|
|
186
183
|
const uncompressedString = stringifyJSON(snapshot);
|
|
@@ -200,76 +197,80 @@ class SessionRecorder {
|
|
|
200
197
|
* necessary management of the buffer and it's related
|
|
201
198
|
* variables
|
|
202
199
|
*/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
|
200
|
+
handleFragPost() {
|
|
201
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
202
|
+
// check if inactive before any processing
|
|
203
|
+
if (yield MetroplexSocket.getInstance().closeIfInactive()) {
|
|
204
|
+
return;
|
|
222
205
|
}
|
|
223
|
-
|
|
224
|
-
|
|
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;
|
|
206
|
+
if (!this.didSetupRecorder) {
|
|
207
|
+
return;
|
|
234
208
|
}
|
|
235
|
-
this.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
209
|
+
if (this.eventBuffer.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
let totalVideoTime = 0;
|
|
214
|
+
// checking if we have those values set in the first place
|
|
215
|
+
if (this.firstRecordedTimestamp &&
|
|
216
|
+
this.lastRecordedTimestamp &&
|
|
217
|
+
!this.isVideoLengthNegativeInvalid) {
|
|
218
|
+
totalVideoTime =
|
|
219
|
+
this.lastRecordedTimestamp - this.firstRecordedTimestamp;
|
|
220
|
+
}
|
|
221
|
+
// In the past we have seen the video LengthMS field to be negative
|
|
222
|
+
// and bigger than the long limit of scala. Which is less than the
|
|
223
|
+
// safe integer limit of js.
|
|
224
|
+
if (!this.isVideoLengthNegativeInvalid &&
|
|
225
|
+
(totalVideoTime < 0 || totalVideoTime >= Number.MAX_SAFE_INTEGER)) {
|
|
226
|
+
// we log an error to know if this is still happening
|
|
227
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`video lengthMS is invalid: ${totalVideoTime}, ` +
|
|
228
|
+
`start time: ${this.firstRecordedTimestamp}, ` +
|
|
229
|
+
`end time: ${this.lastRecordedTimestamp}`, false, SEVERITY.error);
|
|
230
|
+
this.isVideoLengthNegativeInvalid = true;
|
|
231
|
+
totalVideoTime = 0;
|
|
232
|
+
}
|
|
233
|
+
this.vfCounter += 1;
|
|
234
|
+
const videoFragment = MetroplexSocket.getInstance().addEndTimeToPayload({
|
|
235
|
+
// single string for the video content. This gets converted to bytes
|
|
236
|
+
// when being sent to metroplex which we will then unmarshall into
|
|
237
|
+
// a struct to parse it's inner urls
|
|
238
|
+
// If stringifying this event buffer takes too long consider using a service worker
|
|
239
|
+
[VIDEO_FRAG_ATT_NAME]: stringifyJSON(this.eventBuffer),
|
|
240
|
+
// Send the sequence number but don't send the expected length since that is sent as
|
|
241
|
+
// part of the last stored metrics data
|
|
242
|
+
[PV_SEQ_ATT_NAME]: this.vfCounter,
|
|
243
|
+
[LENGTH_ATT_NAME]: totalVideoTime,
|
|
244
|
+
[CSS_URLS_ATT_NAME]: [],
|
|
245
|
+
}, false);
|
|
246
|
+
StoredMetrics.getInstance().addVideoFragData(this.vfCounter, totalVideoTime);
|
|
247
|
+
// constructing a client message that metroplex knows how to handle.
|
|
248
|
+
yield MetroplexSocket.getInstance().sendMessage(VIDEO_METROPLEX_TYPE, {
|
|
249
|
+
[PAGE_VISIT_VID_FRAG_ATT_NAME]: videoFragment,
|
|
250
|
+
});
|
|
251
|
+
this.lastFragPostTimestamp = Date.now();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
// letting collect know we are closing the rrweb listener
|
|
255
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`video frag socket closed with err: ${err.message}`, false, SEVERITY.error);
|
|
256
|
+
// if we detect an error in the frag posting, we stop recording
|
|
257
|
+
// the video
|
|
258
|
+
this.freeze();
|
|
259
|
+
}
|
|
260
|
+
this.eventBuffer = [];
|
|
261
|
+
});
|
|
263
262
|
}
|
|
264
263
|
/**
|
|
265
264
|
* unfreeze forcefully resumes recording events in case it was frozen
|
|
266
265
|
* waiting for user events
|
|
267
266
|
*/
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
unfreeze() {
|
|
268
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
269
|
+
if (this.freezingEvents) {
|
|
270
|
+
this.didSetupRecorder = false;
|
|
271
|
+
yield this.recordUserSession();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
273
274
|
}
|
|
274
275
|
/** stops recording */
|
|
275
276
|
freeze() {
|