@grafana/faro-react-native 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +176 -30
  2. package/android/build.gradle +17 -12
  3. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
  4. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
  5. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
  9. package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
  10. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
  11. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
  12. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
  13. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
  14. package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
  15. package/dist/cjs/config/getRNInstrumentations.js +6 -8
  16. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  17. package/dist/cjs/config/sampling.js.map +1 -1
  18. package/dist/cjs/config/types.js.map +1 -1
  19. package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
  20. package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
  21. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  22. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  23. package/dist/cjs/index.js +8 -3
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
  26. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  27. package/dist/cjs/instrumentations/appState/index.js +8 -2
  28. package/dist/cjs/instrumentations/appState/index.js.map +1 -1
  29. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
  30. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  31. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
  32. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  33. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  34. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  35. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  36. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/errors/index.js +8 -3
  39. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
  41. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  42. package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
  43. package/dist/cjs/instrumentations/performance/index.js +2 -6
  44. package/dist/cjs/instrumentations/performance/index.js.map +1 -1
  45. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
  46. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  47. package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
  48. package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
  49. package/dist/cjs/instrumentations/session/index.js +214 -45
  50. package/dist/cjs/instrumentations/session/index.js.map +1 -1
  51. package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
  52. package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
  53. package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
  54. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  55. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
  56. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  57. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
  58. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  59. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
  60. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  61. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  62. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  63. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
  64. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  65. package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
  66. package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
  67. package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
  68. package/dist/cjs/instrumentations/startup/index.js +2 -6
  69. package/dist/cjs/instrumentations/startup/index.js.map +1 -1
  70. package/dist/cjs/navigation/utils.js +3 -1
  71. package/dist/cjs/navigation/utils.js.map +1 -1
  72. package/dist/cjs/transports/fetch/index.js.map +1 -1
  73. package/dist/cjs/transports/fetch/transport.js +56 -25
  74. package/dist/cjs/transports/fetch/transport.js.map +1 -1
  75. package/dist/cjs/transports/fetch/types.js.map +1 -1
  76. package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
  77. package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
  78. package/dist/cjs/transports/offline/OfflineCache.js +2 -2
  79. package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
  80. package/dist/cjs/transports/offline/transport.js +3 -4
  81. package/dist/cjs/transports/offline/transport.js.map +1 -1
  82. package/dist/cjs/userPersistence/UserPersistence.js +1 -2
  83. package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
  84. package/dist/esm/config/getRNInstrumentations.js +6 -8
  85. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  86. package/dist/esm/config/sampling.js.map +1 -1
  87. package/dist/esm/config/types.js.map +1 -1
  88. package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
  89. package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
  90. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  91. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  92. package/dist/esm/index.js +4 -2
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
  95. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  96. package/dist/esm/instrumentations/appState/index.js +8 -2
  97. package/dist/esm/instrumentations/appState/index.js.map +1 -1
  98. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
  99. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  100. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
  101. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  102. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  103. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  104. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  105. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  106. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  107. package/dist/esm/instrumentations/errors/index.js +8 -3
  108. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  109. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
  110. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  111. package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
  112. package/dist/esm/instrumentations/performance/index.js +2 -6
  113. package/dist/esm/instrumentations/performance/index.js.map +1 -1
  114. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
  115. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  116. package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
  117. package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
  118. package/dist/esm/instrumentations/session/index.js +207 -46
  119. package/dist/esm/instrumentations/session/index.js.map +1 -1
  120. package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
  121. package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
  122. package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
  123. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  124. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
  125. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  126. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
  127. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  128. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
  129. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  130. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  131. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  132. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
  133. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  134. package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
  135. package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
  136. package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
  137. package/dist/esm/instrumentations/startup/index.js +2 -6
  138. package/dist/esm/instrumentations/startup/index.js.map +1 -1
  139. package/dist/esm/navigation/utils.js +2 -1
  140. package/dist/esm/navigation/utils.js.map +1 -1
  141. package/dist/esm/transports/fetch/index.js.map +1 -1
  142. package/dist/esm/transports/fetch/transport.js +38 -14
  143. package/dist/esm/transports/fetch/transport.js.map +1 -1
  144. package/dist/esm/transports/fetch/types.js.map +1 -1
  145. package/dist/esm/transports/offline/ConnectivityService.js +1 -1
  146. package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
  147. package/dist/esm/transports/offline/OfflineCache.js +2 -2
  148. package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
  149. package/dist/esm/transports/offline/transport.js +3 -4
  150. package/dist/esm/transports/offline/transport.js.map +1 -1
  151. package/dist/esm/userPersistence/UserPersistence.js +1 -2
  152. package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
  153. package/dist/types/config/getRNInstrumentations.d.ts +1 -2
  154. package/dist/types/config/sampling.d.ts +0 -3
  155. package/dist/types/config/types.d.ts +5 -3
  156. package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
  157. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  158. package/dist/types/index.d.ts +4 -1
  159. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  160. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  161. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
  162. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
  163. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  164. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
  165. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  166. package/dist/types/instrumentations/performance/index.d.ts +0 -2
  167. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  168. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  169. package/dist/types/instrumentations/session/index.d.ts +20 -1
  170. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  171. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  172. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  173. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  174. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  175. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  176. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  177. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  178. package/dist/types/instrumentations/startup/index.d.ts +2 -6
  179. package/dist/types/navigation/utils.d.ts +1 -0
  180. package/dist/types/transports/fetch/index.d.ts +1 -1
  181. package/dist/types/transports/fetch/transport.d.ts +8 -1
  182. package/dist/types/transports/fetch/types.d.ts +5 -0
  183. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  184. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  185. package/dist/types/transports/offline/transport.d.ts +3 -4
  186. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  187. package/ios/FaroCrashReporter.swift +214 -21
  188. package/ios/FaroReactNative.swift +24 -0
  189. package/ios/FaroReactNativeModule.h +2 -1
  190. package/ios/FaroReactNativeModule.mm +121 -6
  191. package/ios/RefreshRateVitals.swift +34 -38
  192. package/package.json +7 -5
  193. package/src/config/getRNInstrumentations.ts +6 -8
  194. package/src/config/makeRNConfig.test.ts +6 -0
  195. package/src/config/sampling.ts +0 -3
  196. package/src/config/types.ts +5 -3
  197. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  198. package/src/generated/faroRNPackageMeta.ts +1 -1
  199. package/src/index.ts +8 -2
  200. package/src/instrumentations/anr/instrumentation.ts +8 -1
  201. package/src/instrumentations/appState/index.ts +10 -2
  202. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  203. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  204. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  205. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  206. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  207. package/src/instrumentations/crashReporting/types.ts +7 -0
  208. package/src/instrumentations/errors/index.ts +12 -4
  209. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  210. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  211. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  212. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  213. package/src/instrumentations/performance/index.ts +2 -6
  214. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  215. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  216. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  217. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  218. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  219. package/src/instrumentations/session/index.ts +269 -46
  220. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  221. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  222. package/src/instrumentations/session/sessionActivity.ts +58 -0
  223. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  224. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  225. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  226. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  227. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  228. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  229. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  230. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  231. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  232. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  233. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  234. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  235. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  236. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  237. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  238. package/src/instrumentations/session/sessionProcess.ts +114 -0
  239. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  240. package/src/instrumentations/startup/index.ts +2 -6
  241. package/src/navigation/utils.ts +3 -1
  242. package/src/transports/fetch/index.ts +6 -1
  243. package/src/transports/fetch/transport.test.ts +86 -2
  244. package/src/transports/fetch/transport.ts +38 -16
  245. package/src/transports/fetch/types.ts +7 -0
  246. package/src/transports/offline/ConnectivityService.ts +1 -1
  247. package/src/transports/offline/OfflineCache.ts +2 -2
  248. package/src/transports/offline/transport.ts +3 -4
  249. package/src/userPersistence/UserPersistence.ts +1 -2
  250. package/dist/cjs/utils/throttle.js +0 -42
  251. package/dist/cjs/utils/throttle.js.map +0 -1
  252. package/dist/esm/utils/throttle.js +0 -34
  253. package/dist/esm/utils/throttle.js.map +0 -1
  254. package/dist/types/utils/throttle.d.ts +0 -9
  255. package/src/utils/throttle.test.ts +0 -141
  256. 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 { faro, stringifyExternalJson } from '@grafana/faro-core';
4
+ import { dateNow, faro } from '@grafana/faro-core';
5
+ import type { Metas } from '@grafana/faro-core';
5
6
 
6
- import { throttle } from '../../../utils/throttle';
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 { getSessionMetaUpdateHandler, getUserSessionUpdater } from './sessionManagerUtils';
16
+ import {
17
+ getSessionMetaUpdateHandler,
18
+ getUserSessionResetter,
19
+ getUserSessionUpdater,
20
+ toStorableSessionMeta,
21
+ } from './sessionManagerUtils';
10
22
  import type { FaroUserSession } from './types';
11
23
 
12
- function createMmkvInstance(): MMKV {
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
- const { MMKV } = require('react-native-mmkv');
15
- return new MMKV({ id: 'grafana-faro-react-native-session' });
16
- } catch {
41
+ return require('react-native-mmkv') as MmkvModule;
42
+ } catch (error) {
17
43
  throw new Error(
18
- 'sessionTracking.persistent is true but react-native-mmkv could not be loaded. Install it: yarn add react-native-mmkv, then rebuild native projects.'
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
- let mmkvSingleton: MMKV | undefined;
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
- function getMmkv(): MMKV {
26
- if (mmkvSingleton == null) {
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 metaUnsubscribe: (() => void) | null = null;
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
- getMmkv().remove(STORAGE_KEY);
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
- getMmkv().set(STORAGE_KEY, stringifyExternalJson(session));
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 = getMmkv().getString(STORAGE_KEY);
74
- if (storedSession) {
75
- return JSON.parse(storedSession) as FaroUserSession;
223
+ const storedSession = mmkv.getString(STORAGE_KEY);
224
+ if (storedSession == null) {
225
+ runtimeSession = null;
226
+ return null;
76
227
  }
77
- return null;
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
- updateSession = throttle(() => this.updateUserSession(), STORAGE_UPDATE_DELAY);
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 === 'active') {
88
- this.updateSession();
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 unsubscribe = faro.metas.addListener(
96
- getSessionMetaUpdateHandler({
97
- fetchUserSession: MmkvPersistentSessionsManager.fetchUserSession,
98
- storeUserSession: MmkvPersistentSessionsManager.storeUserSession,
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
- if (this.metaUnsubscribe) {
111
- this.metaUnsubscribe();
112
- this.metaUnsubscribe = null;
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 { throttle } from '../../../utils/throttle';
6
+ import { SessionActivityKind } from '../sessionActivity';
6
7
 
7
- import { STORAGE_UPDATE_DELAY } from './sessionConstants';
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 metaUnsubscribe: (() => void) | null = null;
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
- updateSession = throttle(() => this.updateUserSession(), STORAGE_UPDATE_DELAY);
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 === 'active') {
42
- this.updateSession();
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 unsubscribe = faro.metas.addListener(
52
- getSessionMetaUpdateHandler({
53
- fetchUserSession: VolatileSessionsManager.fetchUserSession,
54
- storeUserSession: VolatileSessionsManager.storeUserSession,
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
- if (this.metaUnsubscribe) {
70
- this.metaUnsubscribe();
71
- this.metaUnsubscribe = null;
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 (Flutter-style)', () => {
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: false,
12
+ persistent: true,
13
13
  maxSessionPersistenceTime: MAX_SESSION_PERSISTENCE_TIME,
14
14
  };