@grafana/faro-react-native 1.3.1 → 1.4.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/README.md +176 -30
- package/android/build.gradle +17 -12
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
- package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
- package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
- package/dist/cjs/config/getRNInstrumentations.js +6 -8
- package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
- package/dist/cjs/config/makeRNConfig.js +5 -5
- package/dist/cjs/config/makeRNConfig.js.map +1 -1
- package/dist/cjs/config/sampling.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/appState/index.js +8 -2
- package/dist/cjs/instrumentations/appState/index.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/cjs/instrumentations/errors/index.js +8 -3
- package/dist/cjs/instrumentations/errors/index.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/cjs/instrumentations/performance/index.js +2 -6
- package/dist/cjs/instrumentations/performance/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/index.js +214 -45
- package/dist/cjs/instrumentations/session/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
- package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
- package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
- package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/cjs/instrumentations/startup/index.js +2 -6
- package/dist/cjs/instrumentations/startup/index.js.map +1 -1
- package/dist/cjs/navigation/utils.js +3 -1
- package/dist/cjs/navigation/utils.js.map +1 -1
- package/dist/cjs/transports/fetch/index.js.map +1 -1
- package/dist/cjs/transports/fetch/transport.js +56 -25
- package/dist/cjs/transports/fetch/transport.js.map +1 -1
- package/dist/cjs/transports/fetch/types.js.map +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/cjs/transports/offline/OfflineCache.js +2 -2
- package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
- package/dist/cjs/transports/offline/transport.js +3 -4
- package/dist/cjs/transports/offline/transport.js.map +1 -1
- package/dist/cjs/userPersistence/UserPersistence.js +1 -2
- package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
- package/dist/esm/config/getRNInstrumentations.js +6 -8
- package/dist/esm/config/getRNInstrumentations.js.map +1 -1
- package/dist/esm/config/makeRNConfig.js +5 -5
- package/dist/esm/config/makeRNConfig.js.map +1 -1
- package/dist/esm/config/sampling.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
- package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/appState/index.js +8 -2
- package/dist/esm/instrumentations/appState/index.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/esm/instrumentations/errors/index.js +8 -3
- package/dist/esm/instrumentations/errors/index.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/esm/instrumentations/performance/index.js +2 -6
- package/dist/esm/instrumentations/performance/index.js.map +1 -1
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/index.js +207 -46
- package/dist/esm/instrumentations/session/index.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
- package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
- package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
- package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/esm/instrumentations/startup/index.js +2 -6
- package/dist/esm/instrumentations/startup/index.js.map +1 -1
- package/dist/esm/navigation/utils.js +2 -1
- package/dist/esm/navigation/utils.js.map +1 -1
- package/dist/esm/transports/fetch/index.js.map +1 -1
- package/dist/esm/transports/fetch/transport.js +38 -14
- package/dist/esm/transports/fetch/transport.js.map +1 -1
- package/dist/esm/transports/fetch/types.js.map +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/esm/transports/offline/OfflineCache.js +2 -2
- package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
- package/dist/esm/transports/offline/transport.js +3 -4
- package/dist/esm/transports/offline/transport.js.map +1 -1
- package/dist/esm/userPersistence/UserPersistence.js +1 -2
- package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
- package/dist/types/config/getRNInstrumentations.d.ts +1 -2
- package/dist/types/config/sampling.d.ts +0 -3
- package/dist/types/config/types.d.ts +5 -3
- package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
- package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
- package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
- package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
- package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
- package/dist/types/instrumentations/performance/index.d.ts +0 -2
- package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
- package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
- package/dist/types/instrumentations/session/index.d.ts +20 -1
- package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
- package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
- package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
- package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
- package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
- package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
- package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
- package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
- package/dist/types/instrumentations/startup/index.d.ts +2 -6
- package/dist/types/navigation/utils.d.ts +1 -0
- package/dist/types/transports/fetch/index.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +8 -1
- package/dist/types/transports/fetch/types.d.ts +5 -0
- package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
- package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
- package/dist/types/transports/offline/transport.d.ts +3 -4
- package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
- package/ios/FaroCrashReporter.swift +214 -21
- package/ios/FaroReactNative.swift +24 -0
- package/ios/FaroReactNativeModule.h +2 -1
- package/ios/FaroReactNativeModule.mm +121 -6
- package/ios/RefreshRateVitals.swift +34 -38
- package/package.json +7 -5
- package/src/config/getRNInstrumentations.ts +6 -8
- package/src/config/makeRNConfig.test.ts +6 -0
- package/src/config/makeRNConfig.ts +10 -9
- package/src/config/sampling.ts +0 -3
- package/src/config/types.ts +5 -3
- package/src/dataCollection/DataCollectionPolicy.ts +1 -2
- package/src/generated/faroRNPackageMeta.ts +1 -1
- package/src/index.ts +8 -2
- package/src/initialize.test.ts +63 -2
- package/src/instrumentations/anr/instrumentation.ts +8 -1
- package/src/instrumentations/appState/index.ts +10 -2
- package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
- package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
- package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
- package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/types.ts +7 -0
- package/src/instrumentations/errors/index.ts +12 -4
- package/src/instrumentations/errors/instrumentation.test.ts +35 -0
- package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
- package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
- package/src/instrumentations/frameMonitoring/types.ts +4 -6
- package/src/instrumentations/performance/index.ts +2 -6
- package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
- package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
- package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
- package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
- package/src/instrumentations/session/directSessionActivity.ts +36 -0
- package/src/instrumentations/session/index.ts +269 -46
- package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
- package/src/instrumentations/session/sessionActivity.test.ts +68 -0
- package/src/instrumentations/session/sessionActivity.ts +58 -0
- package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
- package/src/instrumentations/session/sessionAttributes.ts +12 -6
- package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
- package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
- package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
- package/src/instrumentations/session/sessionManager/types.ts +8 -0
- package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
- package/src/instrumentations/session/sessionProcess.test.ts +136 -0
- package/src/instrumentations/session/sessionProcess.ts +114 -0
- package/src/instrumentations/session/startNewSession.test.ts +337 -0
- package/src/instrumentations/startup/index.ts +2 -6
- package/src/navigation/utils.ts +3 -1
- package/src/transports/fetch/index.ts +6 -1
- package/src/transports/fetch/transport.test.ts +86 -2
- package/src/transports/fetch/transport.ts +38 -16
- package/src/transports/fetch/types.ts +7 -0
- package/src/transports/offline/ConnectivityService.ts +1 -1
- package/src/transports/offline/OfflineCache.ts +2 -2
- package/src/transports/offline/transport.ts +3 -4
- package/src/userPersistence/UserPersistence.ts +1 -2
- package/dist/cjs/utils/throttle.js +0 -42
- package/dist/cjs/utils/throttle.js.map +0 -1
- package/dist/esm/utils/throttle.js +0 -34
- package/dist/esm/utils/throttle.js.map +0 -1
- package/dist/types/utils/throttle.d.ts +0 -9
- package/src/utils/throttle.test.ts +0 -141
- package/src/utils/throttle.ts +0 -34
|
@@ -1,37 +1,141 @@
|
|
|
1
|
-
import { AppState, type AppStateStatus } from 'react-native';
|
|
1
|
+
import { AppState, type AppStateStatus, Platform } from 'react-native';
|
|
2
2
|
import type { MMKV } from 'react-native-mmkv';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { dateNow, faro } from '@grafana/faro-core';
|
|
5
|
+
import type { Metas } from '@grafana/faro-core';
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { SessionActivityKind } from '../sessionActivity';
|
|
8
|
+
import {
|
|
9
|
+
claimSessionPersistenceStorageId,
|
|
10
|
+
getSessionProcessInfo,
|
|
11
|
+
releaseSessionPersistenceOwnership,
|
|
12
|
+
} from '../sessionProcess';
|
|
7
13
|
|
|
14
|
+
import { parsePersistentSession, serializePersistentSession } from './persistentSessionRecord';
|
|
8
15
|
import { STORAGE_KEY, STORAGE_UPDATE_DELAY } from './sessionConstants';
|
|
9
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getSessionMetaUpdateHandler,
|
|
18
|
+
getUserSessionResetter,
|
|
19
|
+
getUserSessionUpdater,
|
|
20
|
+
toStorableSessionMeta,
|
|
21
|
+
} from './sessionManagerUtils';
|
|
10
22
|
import type { FaroUserSession } from './types';
|
|
11
23
|
|
|
12
|
-
|
|
24
|
+
type MmkvConfiguration = {
|
|
25
|
+
id: string;
|
|
26
|
+
mode?: number | 'multi-process';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type MmkvModule = {
|
|
30
|
+
createMMKV?: (configuration: MmkvConfiguration) => MMKV;
|
|
31
|
+
MMKV?: new (configuration: MmkvConfiguration) => MMKV;
|
|
32
|
+
Mode?: { MULTI_PROCESS?: number };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function errorMessage(error: unknown): string {
|
|
36
|
+
return error instanceof Error ? error.message : String(error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadMmkvModule(): MmkvModule {
|
|
13
40
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} catch {
|
|
41
|
+
return require('react-native-mmkv') as MmkvModule;
|
|
42
|
+
} catch (error) {
|
|
17
43
|
throw new Error(
|
|
18
|
-
|
|
44
|
+
`react-native-mmkv could not be loaded. Install it and rebuild the native project. ${errorMessage(error)}`
|
|
19
45
|
);
|
|
20
46
|
}
|
|
21
47
|
}
|
|
22
48
|
|
|
23
|
-
|
|
49
|
+
function mmkvConfiguration(mmkv: MmkvModule, storageId: string, isIosExtension: boolean): MmkvConfiguration {
|
|
50
|
+
if (!isIosExtension) {
|
|
51
|
+
return { id: storageId };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof mmkv.createMMKV === 'function') {
|
|
55
|
+
return { id: storageId, mode: 'multi-process' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const legacyMultiProcessMode = mmkv.Mode?.MULTI_PROCESS;
|
|
59
|
+
if (typeof legacyMultiProcessMode !== 'number') {
|
|
60
|
+
throw new Error('Persisting sessions in an iOS extension requires react-native-mmkv v3 or newer.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { id: storageId, mode: legacyMultiProcessMode };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createMmkvInstance(): MMKV {
|
|
67
|
+
// Load MMKV before taking native ownership so a missing dependency cannot
|
|
68
|
+
// leave the process-wide claim occupied.
|
|
69
|
+
const mmkv = loadMmkvModule();
|
|
70
|
+
const processInfo = getSessionProcessInfo();
|
|
71
|
+
const storageId = claimSessionPersistenceStorageId();
|
|
72
|
+
if (storageId == null || processInfo == null) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Native session-storage coordination is unavailable or already owned by another runtime. Rebuild the native app after upgrading Faro, or use sessionTracking.persistent=false in runtimes that cannot own storage.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
24
77
|
|
|
25
|
-
|
|
26
|
-
|
|
78
|
+
try {
|
|
79
|
+
const configuration = mmkvConfiguration(mmkv, storageId, Platform.OS === 'ios' && !processInfo.isMain);
|
|
80
|
+
// react-native-mmkv v4 was rewritten to Nitro and removed the `new MMKV()`
|
|
81
|
+
// class constructor in favor of a `createMMKV()` factory (see the v4 upgrade
|
|
82
|
+
// guide). Support both so v2/v3 (class) and v4+ (factory) work.
|
|
83
|
+
if (typeof mmkv.createMMKV === 'function') {
|
|
84
|
+
return mmkv.createMMKV(configuration);
|
|
85
|
+
}
|
|
86
|
+
if (typeof mmkv.MMKV !== 'function') {
|
|
87
|
+
throw new Error('The installed react-native-mmkv package does not expose a supported storage API.');
|
|
88
|
+
}
|
|
89
|
+
return new mmkv.MMKV(configuration);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
releaseSessionPersistenceOwnership();
|
|
92
|
+
throw new Error(`Session persistence could not be initialized. ${errorMessage(error)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// undefined means MMKV has not been resolved; null means it is unavailable.
|
|
97
|
+
let mmkvSingleton: MMKV | null | undefined;
|
|
98
|
+
// undefined means storage has not been read, null means no persisted state is
|
|
99
|
+
// available, and a session is the live value for this JavaScript runtime.
|
|
100
|
+
let runtimeSession: FaroUserSession | null | undefined;
|
|
101
|
+
|
|
102
|
+
function tryGetMmkv(): MMKV | null {
|
|
103
|
+
if (mmkvSingleton !== undefined) {
|
|
104
|
+
return mmkvSingleton;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
27
108
|
mmkvSingleton = createMmkvInstance();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
mmkvSingleton = null;
|
|
111
|
+
faro.unpatchedConsole?.warn?.('Session persistence is unavailable; using an in-memory session:', error);
|
|
28
112
|
}
|
|
113
|
+
|
|
29
114
|
return mmkvSingleton;
|
|
30
115
|
}
|
|
31
116
|
|
|
117
|
+
function removeMmkvValue(mmkv: MMKV, key: string): void {
|
|
118
|
+
const compatibleMmkv = mmkv as MMKV & {
|
|
119
|
+
delete?: (storageKey: string) => void;
|
|
120
|
+
remove?: (storageKey: string) => void;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (typeof compatibleMmkv.remove === 'function') {
|
|
124
|
+
compatibleMmkv.remove(key);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (typeof compatibleMmkv.delete === 'function') {
|
|
128
|
+
compatibleMmkv.delete(key);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error('The installed react-native-mmkv version cannot remove stored values.');
|
|
133
|
+
}
|
|
134
|
+
|
|
32
135
|
/** @internal */
|
|
33
136
|
export function resetMmkvSingletonForTests(): void {
|
|
34
137
|
mmkvSingleton = undefined;
|
|
138
|
+
runtimeSession = undefined;
|
|
35
139
|
}
|
|
36
140
|
|
|
37
141
|
/**
|
|
@@ -39,12 +143,24 @@ export function resetMmkvSingletonForTests(): void {
|
|
|
39
143
|
* Used when `sessionTracking.persistent` is true.
|
|
40
144
|
*/
|
|
41
145
|
export class MmkvPersistentSessionsManager {
|
|
146
|
+
private resetUserSession: ReturnType<typeof getUserSessionResetter>;
|
|
42
147
|
private updateUserSession: ReturnType<typeof getUserSessionUpdater>;
|
|
43
148
|
private appStateSubscription: ReturnType<typeof AppState.addEventListener> | null = null;
|
|
44
|
-
private
|
|
149
|
+
private activityWriteTimer: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
+
private hasPendingActivityWrite = false;
|
|
151
|
+
private lastActivityWrite = 0;
|
|
152
|
+
private wasBackgrounded = AppState.currentState === 'background';
|
|
153
|
+
private metaUpdateHandler: ReturnType<typeof getSessionMetaUpdateHandler> | null = null;
|
|
154
|
+
private readonly metas: Metas;
|
|
45
155
|
|
|
46
|
-
constructor() {
|
|
156
|
+
constructor(metas: Metas = faro.metas) {
|
|
157
|
+
this.metas = metas;
|
|
47
158
|
this.updateUserSession = getUserSessionUpdater({
|
|
159
|
+
fetchUserSession: MmkvPersistentSessionsManager.fetchUserSession,
|
|
160
|
+
recordUserSessionActivity: this.recordUserSessionActivity,
|
|
161
|
+
storeUserSession: MmkvPersistentSessionsManager.storeUserSession,
|
|
162
|
+
});
|
|
163
|
+
this.resetUserSession = getUserSessionResetter({
|
|
48
164
|
fetchUserSession: MmkvPersistentSessionsManager.fetchUserSession,
|
|
49
165
|
storeUserSession: MmkvPersistentSessionsManager.storeUserSession,
|
|
50
166
|
});
|
|
@@ -53,63 +169,163 @@ export class MmkvPersistentSessionsManager {
|
|
|
53
169
|
}
|
|
54
170
|
|
|
55
171
|
static removeUserSession(): void {
|
|
172
|
+
runtimeSession = null;
|
|
173
|
+
const mmkv = tryGetMmkv();
|
|
174
|
+
if (mmkv == null) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
56
177
|
try {
|
|
57
|
-
|
|
178
|
+
removeMmkvValue(mmkv, STORAGE_KEY);
|
|
58
179
|
} catch (error) {
|
|
59
180
|
faro.unpatchedConsole?.warn?.('Failed to remove session from MMKV:', error);
|
|
60
181
|
}
|
|
61
182
|
}
|
|
62
183
|
|
|
63
184
|
static storeUserSession(session: FaroUserSession): void {
|
|
185
|
+
MmkvPersistentSessionsManager.setRuntimeSession(session);
|
|
186
|
+
MmkvPersistentSessionsManager.persistSession(session);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private static setRuntimeSession(session: FaroUserSession): void {
|
|
190
|
+
runtimeSession =
|
|
191
|
+
session.sessionMeta == null
|
|
192
|
+
? session
|
|
193
|
+
: {
|
|
194
|
+
...session,
|
|
195
|
+
sessionMeta: toStorableSessionMeta(session.sessionMeta),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private static persistSession(session: FaroUserSession): void {
|
|
200
|
+
const mmkv = tryGetMmkv();
|
|
201
|
+
if (mmkv == null) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
64
204
|
try {
|
|
65
|
-
|
|
205
|
+
mmkv.set(STORAGE_KEY, serializePersistentSession(session));
|
|
66
206
|
} catch (error) {
|
|
67
207
|
faro.unpatchedConsole?.warn?.('Failed to store session in MMKV:', error);
|
|
68
208
|
}
|
|
69
209
|
}
|
|
70
210
|
|
|
71
211
|
static fetchUserSession(): FaroUserSession | null {
|
|
212
|
+
if (runtimeSession !== undefined) {
|
|
213
|
+
return runtimeSession;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const mmkv = tryGetMmkv();
|
|
217
|
+
if (mmkv == null) {
|
|
218
|
+
runtimeSession = null;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
72
222
|
try {
|
|
73
|
-
const storedSession =
|
|
74
|
-
if (storedSession) {
|
|
75
|
-
|
|
223
|
+
const storedSession = mmkv.getString(STORAGE_KEY);
|
|
224
|
+
if (storedSession == null) {
|
|
225
|
+
runtimeSession = null;
|
|
226
|
+
return null;
|
|
76
227
|
}
|
|
77
|
-
|
|
228
|
+
|
|
229
|
+
runtimeSession = parsePersistentSession(storedSession);
|
|
230
|
+
if (runtimeSession == null) {
|
|
231
|
+
removeMmkvValue(mmkv, STORAGE_KEY);
|
|
232
|
+
}
|
|
233
|
+
return runtimeSession;
|
|
78
234
|
} catch (error) {
|
|
235
|
+
runtimeSession = null;
|
|
79
236
|
faro.unpatchedConsole?.warn?.('Failed to fetch session from MMKV:', error);
|
|
80
237
|
return null;
|
|
81
238
|
}
|
|
82
239
|
}
|
|
83
240
|
|
|
84
|
-
|
|
241
|
+
checkSession(activity: SessionActivityKind, currentSession?: FaroUserSession | null): FaroUserSession {
|
|
242
|
+
return this.updateUserSession(activity, currentSession);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
resetSession(): FaroUserSession {
|
|
246
|
+
if (this.activityWriteTimer != null) {
|
|
247
|
+
clearTimeout(this.activityWriteTimer);
|
|
248
|
+
this.activityWriteTimer = null;
|
|
249
|
+
}
|
|
250
|
+
this.hasPendingActivityWrite = false;
|
|
251
|
+
|
|
252
|
+
const session = this.resetUserSession();
|
|
253
|
+
this.lastActivityWrite = dateNow();
|
|
254
|
+
return session;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private flushActivityWrite = (): void => {
|
|
258
|
+
if (this.activityWriteTimer != null) {
|
|
259
|
+
clearTimeout(this.activityWriteTimer);
|
|
260
|
+
this.activityWriteTimer = null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!this.hasPendingActivityWrite) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const session = MmkvPersistentSessionsManager.fetchUserSession();
|
|
268
|
+
this.hasPendingActivityWrite = false;
|
|
269
|
+
if (session == null) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
MmkvPersistentSessionsManager.persistSession(session);
|
|
274
|
+
this.lastActivityWrite = dateNow();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
private recordUserSessionActivity = (session: FaroUserSession): void => {
|
|
278
|
+
// Keep expiry checks exact in memory while limiting native storage writes.
|
|
279
|
+
MmkvPersistentSessionsManager.setRuntimeSession(session);
|
|
280
|
+
this.hasPendingActivityWrite = true;
|
|
281
|
+
|
|
282
|
+
const elapsed = dateNow() - this.lastActivityWrite;
|
|
283
|
+
if (this.lastActivityWrite === 0 || elapsed < 0 || elapsed >= STORAGE_UPDATE_DELAY) {
|
|
284
|
+
this.flushActivityWrite();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (this.activityWriteTimer == null) {
|
|
289
|
+
this.activityWriteTimer = setTimeout(this.flushActivityWrite, STORAGE_UPDATE_DELAY - elapsed);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
85
292
|
|
|
86
293
|
private handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
87
|
-
if (nextAppState === '
|
|
88
|
-
this.
|
|
294
|
+
if (nextAppState === 'background') {
|
|
295
|
+
this.wasBackgrounded = true;
|
|
296
|
+
this.flushActivityWrite();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (nextAppState === 'active' && this.wasBackgrounded) {
|
|
301
|
+
this.wasBackgrounded = false;
|
|
302
|
+
this.checkSession(SessionActivityKind.Meaningful);
|
|
89
303
|
}
|
|
90
304
|
};
|
|
91
305
|
|
|
92
306
|
private init(): void {
|
|
93
307
|
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
|
|
94
308
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.metaUnsubscribe = typeof unsubscribe === 'function' ? unsubscribe : null;
|
|
309
|
+
const metaUpdateHandler = getSessionMetaUpdateHandler({
|
|
310
|
+
fetchUserSession: MmkvPersistentSessionsManager.fetchUserSession,
|
|
311
|
+
storeUserSession: MmkvPersistentSessionsManager.storeUserSession,
|
|
312
|
+
});
|
|
313
|
+
this.metas.addListener(metaUpdateHandler);
|
|
314
|
+
this.metaUpdateHandler = metaUpdateHandler;
|
|
102
315
|
}
|
|
103
316
|
|
|
104
317
|
unpatch(): void {
|
|
318
|
+
this.flushActivityWrite();
|
|
319
|
+
|
|
105
320
|
if (this.appStateSubscription) {
|
|
106
321
|
this.appStateSubscription.remove();
|
|
107
322
|
this.appStateSubscription = null;
|
|
108
323
|
}
|
|
109
324
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
325
|
+
const metaUpdateHandler = this.metaUpdateHandler;
|
|
326
|
+
this.metaUpdateHandler = null;
|
|
327
|
+
if (metaUpdateHandler) {
|
|
328
|
+
this.metas.removeListener(metaUpdateHandler);
|
|
113
329
|
}
|
|
114
330
|
}
|
|
115
331
|
}
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import { AppState, type AppStateStatus } from 'react-native';
|
|
2
2
|
|
|
3
3
|
import { faro } from '@grafana/faro-core';
|
|
4
|
+
import type { Metas } from '@grafana/faro-core';
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { SessionActivityKind } from '../sessionActivity';
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
-
import { getSessionMetaUpdateHandler, getUserSessionUpdater } from './sessionManagerUtils';
|
|
8
|
+
import { getSessionMetaUpdateHandler, getUserSessionResetter, getUserSessionUpdater } from './sessionManagerUtils';
|
|
9
9
|
import type { FaroUserSession } from './types';
|
|
10
10
|
|
|
11
11
|
export class VolatileSessionsManager {
|
|
12
12
|
private static volatileStorage: FaroUserSession | null = null;
|
|
13
|
+
private resetUserSession: ReturnType<typeof getUserSessionResetter>;
|
|
13
14
|
private updateUserSession: ReturnType<typeof getUserSessionUpdater>;
|
|
14
15
|
private appStateSubscription: ReturnType<typeof AppState.addEventListener> | null = null;
|
|
15
|
-
private
|
|
16
|
+
private wasBackgrounded = AppState.currentState === 'background';
|
|
17
|
+
private metaUpdateHandler: ReturnType<typeof getSessionMetaUpdateHandler> | null = null;
|
|
18
|
+
private readonly metas: Metas;
|
|
16
19
|
|
|
17
|
-
constructor() {
|
|
20
|
+
constructor(metas: Metas = faro.metas) {
|
|
21
|
+
this.metas = metas;
|
|
18
22
|
this.updateUserSession = getUserSessionUpdater({
|
|
19
23
|
fetchUserSession: VolatileSessionsManager.fetchUserSession,
|
|
20
24
|
storeUserSession: VolatileSessionsManager.storeUserSession,
|
|
21
25
|
});
|
|
26
|
+
this.resetUserSession = getUserSessionResetter({
|
|
27
|
+
fetchUserSession: VolatileSessionsManager.fetchUserSession,
|
|
28
|
+
storeUserSession: VolatileSessionsManager.storeUserSession,
|
|
29
|
+
});
|
|
22
30
|
|
|
23
31
|
this.init();
|
|
24
32
|
}
|
|
@@ -35,11 +43,23 @@ export class VolatileSessionsManager {
|
|
|
35
43
|
return VolatileSessionsManager.volatileStorage;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
checkSession(activity: SessionActivityKind, currentSession?: FaroUserSession | null): FaroUserSession {
|
|
47
|
+
return this.updateUserSession(activity, currentSession);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resetSession(): FaroUserSession {
|
|
51
|
+
return this.resetUserSession();
|
|
52
|
+
}
|
|
39
53
|
|
|
40
54
|
private handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
41
|
-
if (nextAppState === '
|
|
42
|
-
this.
|
|
55
|
+
if (nextAppState === 'background') {
|
|
56
|
+
this.wasBackgrounded = true;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (nextAppState === 'active' && this.wasBackgrounded) {
|
|
61
|
+
this.wasBackgrounded = false;
|
|
62
|
+
this.checkSession(SessionActivityKind.Meaningful);
|
|
43
63
|
}
|
|
44
64
|
};
|
|
45
65
|
|
|
@@ -48,13 +68,12 @@ export class VolatileSessionsManager {
|
|
|
48
68
|
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
|
|
49
69
|
|
|
50
70
|
// Users can call the setSession() method, so we need to sync this with the in-memory session
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.metaUnsubscribe = typeof unsubscribe === 'function' ? unsubscribe : null;
|
|
71
|
+
const metaUpdateHandler = getSessionMetaUpdateHandler({
|
|
72
|
+
fetchUserSession: VolatileSessionsManager.fetchUserSession,
|
|
73
|
+
storeUserSession: VolatileSessionsManager.storeUserSession,
|
|
74
|
+
});
|
|
75
|
+
this.metas.addListener(metaUpdateHandler);
|
|
76
|
+
this.metaUpdateHandler = metaUpdateHandler;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
/**
|
|
@@ -66,9 +85,10 @@ export class VolatileSessionsManager {
|
|
|
66
85
|
this.appStateSubscription = null;
|
|
67
86
|
}
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
const metaUpdateHandler = this.metaUpdateHandler;
|
|
89
|
+
this.metaUpdateHandler = null;
|
|
90
|
+
if (metaUpdateHandler) {
|
|
91
|
+
this.metas.removeListener(metaUpdateHandler);
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parsePersistentSession,
|
|
3
|
+
PERSISTED_SESSION_SCHEMA_VERSION,
|
|
4
|
+
serializePersistentSession,
|
|
5
|
+
} from './persistentSessionRecord';
|
|
6
|
+
import type { FaroUserSession } from './types';
|
|
7
|
+
|
|
8
|
+
const session: FaroUserSession = {
|
|
9
|
+
sessionId: 'current-session',
|
|
10
|
+
started: 100,
|
|
11
|
+
lastActivity: 200,
|
|
12
|
+
isSampled: false,
|
|
13
|
+
sessionMeta: {
|
|
14
|
+
id: 'current-session',
|
|
15
|
+
attributes: {
|
|
16
|
+
previousSession: 'previous-session',
|
|
17
|
+
device_model: 'Test Phone',
|
|
18
|
+
custom: 'not-persisted',
|
|
19
|
+
},
|
|
20
|
+
overrides: {
|
|
21
|
+
serviceName: 'not-persisted',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('persistentSessionRecord', () => {
|
|
27
|
+
it('serializes only the versioned minimal session state', () => {
|
|
28
|
+
expect(JSON.parse(serializePersistentSession(session))).toStrictEqual({
|
|
29
|
+
schemaVersion: PERSISTED_SESSION_SCHEMA_VERSION,
|
|
30
|
+
currentSessionId: 'current-session',
|
|
31
|
+
previousSessionId: 'previous-session',
|
|
32
|
+
startedAt: 100,
|
|
33
|
+
lastActivityAt: 200,
|
|
34
|
+
isSampled: false,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('round-trips the fields needed for session continuity', () => {
|
|
39
|
+
expect(parsePersistentSession(serializePersistentSession(session))).toStrictEqual({
|
|
40
|
+
sessionId: 'current-session',
|
|
41
|
+
started: 100,
|
|
42
|
+
lastActivity: 200,
|
|
43
|
+
isSampled: false,
|
|
44
|
+
sessionMeta: {
|
|
45
|
+
id: 'current-session',
|
|
46
|
+
attributes: {
|
|
47
|
+
isSampled: 'false',
|
|
48
|
+
previousSession: 'previous-session',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
['missing state', ''],
|
|
56
|
+
['corrupt JSON', '{not-json'],
|
|
57
|
+
[
|
|
58
|
+
'an unsupported schema',
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
schemaVersion: PERSISTED_SESSION_SCHEMA_VERSION + 1,
|
|
61
|
+
currentSessionId: 'current-session',
|
|
62
|
+
previousSessionId: null,
|
|
63
|
+
startedAt: 100,
|
|
64
|
+
lastActivityAt: 200,
|
|
65
|
+
isSampled: true,
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
'an unversioned legacy record',
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
sessionId: 'legacy-session',
|
|
72
|
+
started: 100,
|
|
73
|
+
lastActivity: 200,
|
|
74
|
+
isSampled: true,
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
'invalid timing',
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
schemaVersion: PERSISTED_SESSION_SCHEMA_VERSION,
|
|
81
|
+
currentSessionId: 'current-session',
|
|
82
|
+
previousSessionId: null,
|
|
83
|
+
startedAt: 200,
|
|
84
|
+
lastActivityAt: 100,
|
|
85
|
+
isSampled: true,
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
])('rejects %s', (_name, serialized) => {
|
|
89
|
+
expect(parsePersistentSession(serialized)).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('clamps a backward activity timestamp when serializing', () => {
|
|
93
|
+
expect(
|
|
94
|
+
JSON.parse(
|
|
95
|
+
serializePersistentSession({
|
|
96
|
+
...session,
|
|
97
|
+
started: 200,
|
|
98
|
+
lastActivity: 100,
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
).toMatchObject({ startedAt: 200, lastActivityAt: 200 });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { FaroUserSession } from './types';
|
|
2
|
+
|
|
3
|
+
export const PERSISTED_SESSION_SCHEMA_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
type PersistedSessionRecord = {
|
|
6
|
+
schemaVersion: typeof PERSISTED_SESSION_SCHEMA_VERSION;
|
|
7
|
+
currentSessionId: string;
|
|
8
|
+
previousSessionId: string | null;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
lastActivityAt: number;
|
|
11
|
+
isSampled: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value != null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
19
|
+
return typeof value === 'string' && value.length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isValidTimestamp(value: unknown): value is number {
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function serializePersistentSession(session: FaroUserSession): string {
|
|
27
|
+
const previousSession = session.sessionMeta?.attributes?.['previousSession'];
|
|
28
|
+
const record: PersistedSessionRecord = {
|
|
29
|
+
schemaVersion: PERSISTED_SESSION_SCHEMA_VERSION,
|
|
30
|
+
currentSessionId: session.sessionId,
|
|
31
|
+
previousSessionId: isNonEmptyString(previousSession) ? previousSession : null,
|
|
32
|
+
startedAt: session.started,
|
|
33
|
+
// Preserve the record invariant if the device clock moves backwards after
|
|
34
|
+
// the session starts.
|
|
35
|
+
lastActivityAt: Math.max(session.started, session.lastActivity),
|
|
36
|
+
isSampled: session.isSampled,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return JSON.stringify(record);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parsePersistentSession(serialized: string): FaroUserSession | null {
|
|
43
|
+
let value: unknown;
|
|
44
|
+
try {
|
|
45
|
+
value = JSON.parse(serialized);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
!isRecord(value) ||
|
|
52
|
+
value['schemaVersion'] !== PERSISTED_SESSION_SCHEMA_VERSION ||
|
|
53
|
+
!isNonEmptyString(value['currentSessionId']) ||
|
|
54
|
+
!(value['previousSessionId'] === null || isNonEmptyString(value['previousSessionId'])) ||
|
|
55
|
+
!isValidTimestamp(value['startedAt']) ||
|
|
56
|
+
!isValidTimestamp(value['lastActivityAt']) ||
|
|
57
|
+
value['lastActivityAt'] < value['startedAt'] ||
|
|
58
|
+
typeof value['isSampled'] !== 'boolean'
|
|
59
|
+
) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const previousSessionId = value['previousSessionId'];
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
sessionId: value['currentSessionId'],
|
|
67
|
+
started: value['startedAt'],
|
|
68
|
+
lastActivity: value['lastActivityAt'],
|
|
69
|
+
isSampled: value['isSampled'],
|
|
70
|
+
sessionMeta: {
|
|
71
|
+
id: value['currentSessionId'],
|
|
72
|
+
attributes: {
|
|
73
|
+
isSampled: value['isSampled'].toString(),
|
|
74
|
+
...(previousSessionId == null ? {} : { previousSession: previousSessionId }),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -21,7 +21,7 @@ describe('Sampling', () => {
|
|
|
21
21
|
expect(isSampled()).toBe(true);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
describe('Sampling interface
|
|
24
|
+
describe('Sampling interface', () => {
|
|
25
25
|
it('SamplingRate(1) samples all sessions', () => {
|
|
26
26
|
const config = mockConfig({
|
|
27
27
|
sessionTracking: {
|
|
@@ -9,6 +9,6 @@ export const MAX_SESSION_PERSISTENCE_TIME = DEFAULT_SESSION_PERSISTENCE_MS;
|
|
|
9
9
|
/** React Native session defaults */
|
|
10
10
|
export const defaultSessionTrackingConfig: ReactNativeSessionTrackingConfig = {
|
|
11
11
|
enabled: true,
|
|
12
|
-
persistent:
|
|
12
|
+
persistent: true,
|
|
13
13
|
maxSessionPersistenceTime: MAX_SESSION_PERSISTENCE_TIME,
|
|
14
14
|
};
|