@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.
Files changed (262) 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/makeRNConfig.js +5 -5
  18. package/dist/cjs/config/makeRNConfig.js.map +1 -1
  19. package/dist/cjs/config/sampling.js.map +1 -1
  20. package/dist/cjs/config/types.js.map +1 -1
  21. package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
  22. package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
  23. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  24. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  25. package/dist/cjs/index.js +8 -3
  26. package/dist/cjs/index.js.map +1 -1
  27. package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
  28. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  29. package/dist/cjs/instrumentations/appState/index.js +8 -2
  30. package/dist/cjs/instrumentations/appState/index.js.map +1 -1
  31. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
  32. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  33. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
  34. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  35. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  36. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  38. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  39. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  40. package/dist/cjs/instrumentations/errors/index.js +8 -3
  41. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  42. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
  43. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  44. package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
  45. package/dist/cjs/instrumentations/performance/index.js +2 -6
  46. package/dist/cjs/instrumentations/performance/index.js.map +1 -1
  47. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
  48. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  49. package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
  50. package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
  51. package/dist/cjs/instrumentations/session/index.js +214 -45
  52. package/dist/cjs/instrumentations/session/index.js.map +1 -1
  53. package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
  54. package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
  55. package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
  56. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  57. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
  58. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  59. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
  60. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  61. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
  62. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  63. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  64. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  65. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
  66. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  67. package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
  68. package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
  69. package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
  70. package/dist/cjs/instrumentations/startup/index.js +2 -6
  71. package/dist/cjs/instrumentations/startup/index.js.map +1 -1
  72. package/dist/cjs/navigation/utils.js +3 -1
  73. package/dist/cjs/navigation/utils.js.map +1 -1
  74. package/dist/cjs/transports/fetch/index.js.map +1 -1
  75. package/dist/cjs/transports/fetch/transport.js +56 -25
  76. package/dist/cjs/transports/fetch/transport.js.map +1 -1
  77. package/dist/cjs/transports/fetch/types.js.map +1 -1
  78. package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
  79. package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
  80. package/dist/cjs/transports/offline/OfflineCache.js +2 -2
  81. package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
  82. package/dist/cjs/transports/offline/transport.js +3 -4
  83. package/dist/cjs/transports/offline/transport.js.map +1 -1
  84. package/dist/cjs/userPersistence/UserPersistence.js +1 -2
  85. package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
  86. package/dist/esm/config/getRNInstrumentations.js +6 -8
  87. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  88. package/dist/esm/config/makeRNConfig.js +5 -5
  89. package/dist/esm/config/makeRNConfig.js.map +1 -1
  90. package/dist/esm/config/sampling.js.map +1 -1
  91. package/dist/esm/config/types.js.map +1 -1
  92. package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
  93. package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
  94. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  95. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  96. package/dist/esm/index.js +4 -2
  97. package/dist/esm/index.js.map +1 -1
  98. package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
  99. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  100. package/dist/esm/instrumentations/appState/index.js +8 -2
  101. package/dist/esm/instrumentations/appState/index.js.map +1 -1
  102. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
  103. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  104. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
  105. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  106. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  107. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  108. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  109. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  110. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  111. package/dist/esm/instrumentations/errors/index.js +8 -3
  112. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  113. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
  114. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  115. package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
  116. package/dist/esm/instrumentations/performance/index.js +2 -6
  117. package/dist/esm/instrumentations/performance/index.js.map +1 -1
  118. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
  119. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  120. package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
  121. package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
  122. package/dist/esm/instrumentations/session/index.js +207 -46
  123. package/dist/esm/instrumentations/session/index.js.map +1 -1
  124. package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
  125. package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
  126. package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
  127. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  128. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
  129. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  130. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
  131. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  132. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
  133. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  134. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  135. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  136. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
  137. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  138. package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
  139. package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
  140. package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
  141. package/dist/esm/instrumentations/startup/index.js +2 -6
  142. package/dist/esm/instrumentations/startup/index.js.map +1 -1
  143. package/dist/esm/navigation/utils.js +2 -1
  144. package/dist/esm/navigation/utils.js.map +1 -1
  145. package/dist/esm/transports/fetch/index.js.map +1 -1
  146. package/dist/esm/transports/fetch/transport.js +38 -14
  147. package/dist/esm/transports/fetch/transport.js.map +1 -1
  148. package/dist/esm/transports/fetch/types.js.map +1 -1
  149. package/dist/esm/transports/offline/ConnectivityService.js +1 -1
  150. package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
  151. package/dist/esm/transports/offline/OfflineCache.js +2 -2
  152. package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
  153. package/dist/esm/transports/offline/transport.js +3 -4
  154. package/dist/esm/transports/offline/transport.js.map +1 -1
  155. package/dist/esm/userPersistence/UserPersistence.js +1 -2
  156. package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
  157. package/dist/types/config/getRNInstrumentations.d.ts +1 -2
  158. package/dist/types/config/sampling.d.ts +0 -3
  159. package/dist/types/config/types.d.ts +5 -3
  160. package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
  161. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  162. package/dist/types/index.d.ts +4 -1
  163. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  164. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  165. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
  166. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
  167. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  168. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
  169. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  170. package/dist/types/instrumentations/performance/index.d.ts +0 -2
  171. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  172. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  173. package/dist/types/instrumentations/session/index.d.ts +20 -1
  174. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  175. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  176. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  177. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  178. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  179. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  180. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  181. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  182. package/dist/types/instrumentations/startup/index.d.ts +2 -6
  183. package/dist/types/navigation/utils.d.ts +1 -0
  184. package/dist/types/transports/fetch/index.d.ts +1 -1
  185. package/dist/types/transports/fetch/transport.d.ts +8 -1
  186. package/dist/types/transports/fetch/types.d.ts +5 -0
  187. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  188. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  189. package/dist/types/transports/offline/transport.d.ts +3 -4
  190. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  191. package/ios/FaroCrashReporter.swift +214 -21
  192. package/ios/FaroReactNative.swift +24 -0
  193. package/ios/FaroReactNativeModule.h +2 -1
  194. package/ios/FaroReactNativeModule.mm +121 -6
  195. package/ios/RefreshRateVitals.swift +34 -38
  196. package/package.json +7 -5
  197. package/src/config/getRNInstrumentations.ts +6 -8
  198. package/src/config/makeRNConfig.test.ts +6 -0
  199. package/src/config/makeRNConfig.ts +10 -9
  200. package/src/config/sampling.ts +0 -3
  201. package/src/config/types.ts +5 -3
  202. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  203. package/src/generated/faroRNPackageMeta.ts +1 -1
  204. package/src/index.ts +8 -2
  205. package/src/initialize.test.ts +63 -2
  206. package/src/instrumentations/anr/instrumentation.ts +8 -1
  207. package/src/instrumentations/appState/index.ts +10 -2
  208. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  209. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  210. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  211. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  212. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  213. package/src/instrumentations/crashReporting/types.ts +7 -0
  214. package/src/instrumentations/errors/index.ts +12 -4
  215. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  216. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  217. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  218. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  219. package/src/instrumentations/performance/index.ts +2 -6
  220. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  221. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  222. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  223. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  224. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  225. package/src/instrumentations/session/index.ts +269 -46
  226. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  227. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  228. package/src/instrumentations/session/sessionActivity.ts +58 -0
  229. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  230. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  231. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  232. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  233. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  234. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  235. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  236. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  237. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  238. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  239. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  240. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  241. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  242. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  243. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  244. package/src/instrumentations/session/sessionProcess.ts +114 -0
  245. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  246. package/src/instrumentations/startup/index.ts +2 -6
  247. package/src/navigation/utils.ts +3 -1
  248. package/src/transports/fetch/index.ts +6 -1
  249. package/src/transports/fetch/transport.test.ts +86 -2
  250. package/src/transports/fetch/transport.ts +38 -16
  251. package/src/transports/fetch/types.ts +7 -0
  252. package/src/transports/offline/ConnectivityService.ts +1 -1
  253. package/src/transports/offline/OfflineCache.ts +2 -2
  254. package/src/transports/offline/transport.ts +3 -4
  255. package/src/userPersistence/UserPersistence.ts +1 -2
  256. package/dist/cjs/utils/throttle.js +0 -42
  257. package/dist/cjs/utils/throttle.js.map +0 -1
  258. package/dist/esm/utils/throttle.js +0 -34
  259. package/dist/esm/utils/throttle.js.map +0 -1
  260. package/dist/types/utils/throttle.d.ts +0 -9
  261. package/src/utils/throttle.test.ts +0 -141
  262. package/src/utils/throttle.ts +0 -34
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Regression coverage for the MMKV loader's version compatibility.
3
+ *
4
+ * `createMmkvInstance` is private; it is exercised lazily via the first call to
5
+ * a static that touches storage (`fetchUserSession` -> `getMmkv`). Each test
6
+ * loads the module in an isolated registry with `react-native-mmkv` mocked to a
7
+ * specific export shape, so we can assert which construction path is taken.
8
+ */
9
+
10
+ import { NativeModules, Platform } from 'react-native';
11
+
12
+ const SESSION_MMKV_CONFIG = { id: 'grafana-faro-react-native-session' };
13
+ const DEFAULT_PROCESS_INFO = { identifier: 'com.example.myapp', isMain: true };
14
+ const originalNativeModule = NativeModules.FaroReactNativeModule;
15
+ const originalPlatformOS = Platform.OS;
16
+
17
+ type SessionsManagerModule = typeof import('./MmkvPersistentSessionsManager');
18
+
19
+ const loadWithMmkvMock = (
20
+ mmkvExports: Record<string, unknown>,
21
+ storageId: string | null = SESSION_MMKV_CONFIG.id,
22
+ processInfo: { identifier: string; isMain: boolean } | null = DEFAULT_PROCESS_INFO,
23
+ mockReleasePersistenceOwnership: jest.Mock = jest.fn(() => true)
24
+ ): SessionsManagerModule => {
25
+ let mod: SessionsManagerModule | undefined;
26
+
27
+ jest.isolateModules(() => {
28
+ jest.doMock('react-native-mmkv', () => mmkvExports);
29
+ jest.doMock('../sessionProcess', () => ({
30
+ claimSessionPersistenceStorageId: jest.fn(() => storageId),
31
+ getSessionProcessInfo: jest.fn(() => processInfo),
32
+ releaseSessionPersistenceOwnership: mockReleasePersistenceOwnership,
33
+ }));
34
+ mod = require('./MmkvPersistentSessionsManager');
35
+ });
36
+
37
+ if (!mod) {
38
+ throw new Error('jest.isolateModules did not load MmkvPersistentSessionsManager');
39
+ }
40
+
41
+ return mod;
42
+ };
43
+
44
+ beforeEach(() => {
45
+ NativeModules.FaroReactNativeModule = {
46
+ claimSessionPersistence: () => true,
47
+ getSessionProcessIdentifier: () => 'com.example.myapp',
48
+ isMainSessionProcess: () => true,
49
+ releaseSessionPersistence: () => true,
50
+ };
51
+ });
52
+
53
+ afterEach(() => {
54
+ (Platform as { OS: string }).OS = originalPlatformOS;
55
+ });
56
+
57
+ afterAll(() => {
58
+ NativeModules.FaroReactNativeModule = originalNativeModule;
59
+ });
60
+
61
+ describe('MmkvPersistentSessionsManager - react-native-mmkv version compatibility', () => {
62
+ afterEach(() => {
63
+ jest.dontMock('../sessionProcess');
64
+ jest.resetModules();
65
+ jest.clearAllMocks();
66
+ });
67
+
68
+ it('uses the createMMKV() factory when available (react-native-mmkv v4+)', () => {
69
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
70
+ const createMMKV = jest.fn().mockReturnValue(store);
71
+
72
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ createMMKV });
73
+ MmkvPersistentSessionsManager.fetchUserSession();
74
+
75
+ expect(createMMKV).toHaveBeenCalledWith(SESSION_MMKV_CONFIG);
76
+ });
77
+
78
+ it('falls back to `new MMKV()` when only the class is exported (v2/v3)', () => {
79
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
80
+ const MMKV = jest.fn().mockImplementation(() => store);
81
+
82
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ MMKV });
83
+ MmkvPersistentSessionsManager.fetchUserSession();
84
+
85
+ expect(MMKV).toHaveBeenCalledWith(SESSION_MMKV_CONFIG);
86
+ });
87
+
88
+ it('uses delete() to clean up stored state with react-native-mmkv v2/v3', () => {
89
+ const store = {
90
+ getString: jest.fn().mockReturnValue('{not-json'),
91
+ delete: jest.fn(),
92
+ };
93
+ const MMKV = jest.fn().mockImplementation(() => store);
94
+
95
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ MMKV });
96
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
97
+
98
+ expect(store.delete).toHaveBeenCalledWith('com.grafana.faro.session');
99
+ });
100
+
101
+ it('prefers createMMKV() over the legacy class when both are present', () => {
102
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
103
+ const createMMKV = jest.fn().mockReturnValue(store);
104
+ const MMKV = jest.fn().mockImplementation(() => store);
105
+
106
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ createMMKV, MMKV });
107
+ MmkvPersistentSessionsManager.fetchUserSession();
108
+
109
+ expect(createMMKV).toHaveBeenCalledWith(SESSION_MMKV_CONFIG);
110
+ expect(MMKV).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('uses a separate MMKV id for an Android secondary process', () => {
114
+ (Platform as { OS: string }).OS = 'android';
115
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
116
+ const createMMKV = jest.fn().mockReturnValue(store);
117
+ const processStorageId = 'grafana-faro-react-native-session.com.example.myapp%3Async';
118
+
119
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ createMMKV }, processStorageId, {
120
+ identifier: 'com.example.myapp:sync',
121
+ isMain: false,
122
+ });
123
+ MmkvPersistentSessionsManager.fetchUserSession();
124
+
125
+ expect(createMMKV).toHaveBeenCalledWith({ id: processStorageId });
126
+ });
127
+
128
+ it('uses multi-process mode for iOS extensions with react-native-mmkv v4+', () => {
129
+ (Platform as { OS: string }).OS = 'ios';
130
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
131
+ const createMMKV = jest.fn().mockReturnValue(store);
132
+ const processStorageId = 'grafana-faro-react-native-session.com.example.share';
133
+
134
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ createMMKV }, processStorageId, {
135
+ identifier: 'com.example.share',
136
+ isMain: false,
137
+ });
138
+ MmkvPersistentSessionsManager.fetchUserSession();
139
+
140
+ expect(createMMKV).toHaveBeenCalledWith({ id: processStorageId, mode: 'multi-process' });
141
+ });
142
+
143
+ it('uses multi-process mode for iOS extensions with react-native-mmkv v3', () => {
144
+ (Platform as { OS: string }).OS = 'ios';
145
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
146
+ const MMKV = jest.fn().mockImplementation(() => store);
147
+ const processStorageId = 'grafana-faro-react-native-session.com.example.share';
148
+
149
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ MMKV, Mode: { MULTI_PROCESS: 1 } }, processStorageId, {
150
+ identifier: 'com.example.share',
151
+ isMain: false,
152
+ });
153
+ MmkvPersistentSessionsManager.fetchUserSession();
154
+
155
+ expect(MMKV).toHaveBeenCalledWith({ id: processStorageId, mode: 1 });
156
+ });
157
+
158
+ it('fails closed for iOS extensions when react-native-mmkv lacks multi-process mode', () => {
159
+ (Platform as { OS: string }).OS = 'ios';
160
+ const MMKV = jest.fn();
161
+ const releasePersistenceOwnership = jest.fn(() => true);
162
+
163
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock(
164
+ { MMKV },
165
+ 'grafana-faro-react-native-session.com.example.share',
166
+ { identifier: 'com.example.share', isMain: false },
167
+ releasePersistenceOwnership
168
+ );
169
+
170
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
171
+ expect(MMKV).not.toHaveBeenCalled();
172
+ expect(releasePersistenceOwnership).toHaveBeenCalledTimes(1);
173
+ });
174
+
175
+ it('does not create MMKV when this runtime cannot claim persistence', () => {
176
+ const createMMKV = jest.fn();
177
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({ createMMKV }, null);
178
+ const session = {
179
+ sessionId: 'runtime-session',
180
+ started: 100,
181
+ lastActivity: 200,
182
+ isSampled: true,
183
+ };
184
+
185
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
186
+ MmkvPersistentSessionsManager.storeUserSession(session);
187
+
188
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBe(session);
189
+ expect(createMMKV).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it('keeps independent process session chains in separate MMKV records', () => {
193
+ const stores = new Map<string, { getString: jest.Mock; set: jest.Mock }>();
194
+ const createMMKV = jest.fn(({ id }: { id: string }) => {
195
+ const values = new Map<string, string>();
196
+ const store = {
197
+ getString: jest.fn((key: string) => values.get(key)),
198
+ set: jest.fn((key: string, value: string) => values.set(key, value)),
199
+ };
200
+ stores.set(id, store);
201
+ return store;
202
+ });
203
+ const workerStorageId = 'grafana-faro-react-native-session.com.example.myapp%3Async';
204
+
205
+ const main = loadWithMmkvMock({ createMMKV }, SESSION_MMKV_CONFIG.id);
206
+ main.MmkvPersistentSessionsManager.storeUserSession({
207
+ sessionId: 'main-current',
208
+ started: 100,
209
+ lastActivity: 200,
210
+ isSampled: true,
211
+ sessionMeta: {
212
+ id: 'main-current',
213
+ attributes: { previousSession: 'main-previous' },
214
+ },
215
+ });
216
+
217
+ const worker = loadWithMmkvMock({ createMMKV }, workerStorageId);
218
+ worker.MmkvPersistentSessionsManager.storeUserSession({
219
+ sessionId: 'worker-current',
220
+ started: 300,
221
+ lastActivity: 400,
222
+ isSampled: false,
223
+ sessionMeta: {
224
+ id: 'worker-current',
225
+ attributes: { previousSession: 'worker-previous' },
226
+ },
227
+ });
228
+
229
+ const mainRecord = JSON.parse(stores.get(SESSION_MMKV_CONFIG.id)?.set.mock.calls[0]?.[1] ?? 'null');
230
+ const workerRecord = JSON.parse(stores.get(workerStorageId)?.set.mock.calls[0]?.[1] ?? 'null');
231
+ expect(mainRecord).toMatchObject({
232
+ currentSessionId: 'main-current',
233
+ previousSessionId: 'main-previous',
234
+ });
235
+ expect(workerRecord).toMatchObject({
236
+ currentSessionId: 'worker-current',
237
+ previousSessionId: 'worker-previous',
238
+ });
239
+ });
240
+ });
241
+
242
+ describe('MmkvPersistentSessionsManager - session persistence', () => {
243
+ afterEach(() => {
244
+ jest.dontMock('../sessionProcess');
245
+ jest.resetModules();
246
+ jest.clearAllMocks();
247
+ });
248
+
249
+ it('returns null when no session has been stored', () => {
250
+ const store = { getString: jest.fn().mockReturnValue(undefined) };
251
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({
252
+ createMMKV: jest.fn().mockReturnValue(store),
253
+ });
254
+
255
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
256
+ });
257
+
258
+ it.each([
259
+ ['empty', ''],
260
+ ['corrupt', '{not-json'],
261
+ [
262
+ 'unsupported',
263
+ JSON.stringify({
264
+ schemaVersion: 99,
265
+ currentSessionId: 'old-session',
266
+ previousSessionId: null,
267
+ startedAt: 100,
268
+ lastActivityAt: 200,
269
+ isSampled: true,
270
+ }),
271
+ ],
272
+ ])('removes %s stored state', (_name, serialized) => {
273
+ const store = {
274
+ getString: jest.fn().mockReturnValue(serialized),
275
+ remove: jest.fn(),
276
+ };
277
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({
278
+ createMMKV: jest.fn().mockReturnValue(store),
279
+ });
280
+
281
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
282
+ expect(store.remove).toHaveBeenCalledWith('com.grafana.faro.session');
283
+ });
284
+
285
+ it('stores a minimal record while retaining normalized runtime metadata in memory', () => {
286
+ const store = {
287
+ getString: jest.fn().mockReturnValue(undefined),
288
+ set: jest.fn(),
289
+ };
290
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({
291
+ createMMKV: jest.fn().mockReturnValue(store),
292
+ });
293
+ const session = {
294
+ sessionId: 'current-session',
295
+ started: 100,
296
+ lastActivity: 200,
297
+ isSampled: true,
298
+ sessionMeta: {
299
+ id: 'current-session',
300
+ attributes: {
301
+ previousSession: 'previous-session',
302
+ custom: 'runtime-only',
303
+ unavailable: undefined,
304
+ },
305
+ overrides: {
306
+ serviceName: 'runtime-only',
307
+ },
308
+ },
309
+ };
310
+
311
+ MmkvPersistentSessionsManager.storeUserSession(session);
312
+
313
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toStrictEqual({
314
+ ...session,
315
+ sessionMeta: {
316
+ ...session.sessionMeta,
317
+ attributes: {
318
+ previousSession: 'previous-session',
319
+ custom: 'runtime-only',
320
+ },
321
+ },
322
+ });
323
+ expect(JSON.parse(store.set.mock.calls[0][1])).toStrictEqual({
324
+ schemaVersion: 1,
325
+ currentSessionId: 'current-session',
326
+ previousSessionId: 'previous-session',
327
+ startedAt: 100,
328
+ lastActivityAt: 200,
329
+ isSampled: true,
330
+ });
331
+ });
332
+
333
+ it('resolves unavailable MMKV once and retains the live session in memory', () => {
334
+ const createMMKV = jest.fn(() => {
335
+ throw new Error('native module unavailable');
336
+ });
337
+ const releasePersistenceOwnership = jest.fn(() => true);
338
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock(
339
+ { createMMKV },
340
+ SESSION_MMKV_CONFIG.id,
341
+ DEFAULT_PROCESS_INFO,
342
+ releasePersistenceOwnership
343
+ );
344
+ const session = {
345
+ sessionId: 'current-session',
346
+ started: 100,
347
+ lastActivity: 200,
348
+ isSampled: true,
349
+ };
350
+
351
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
352
+ MmkvPersistentSessionsManager.storeUserSession(session);
353
+
354
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBe(session);
355
+ expect(createMMKV).toHaveBeenCalledTimes(1);
356
+ expect(releasePersistenceOwnership).toHaveBeenCalledTimes(1);
357
+ });
358
+
359
+ it('retains the live session when an MMKV write fails', () => {
360
+ const store = {
361
+ getString: jest.fn().mockReturnValue(undefined),
362
+ set: jest.fn(() => {
363
+ throw new Error('write failed');
364
+ }),
365
+ };
366
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({
367
+ createMMKV: jest.fn().mockReturnValue(store),
368
+ });
369
+ const session = {
370
+ sessionId: 'current-session',
371
+ started: 100,
372
+ lastActivity: 200,
373
+ isSampled: true,
374
+ };
375
+
376
+ MmkvPersistentSessionsManager.storeUserSession(session);
377
+
378
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBe(session);
379
+ expect(store.set).toHaveBeenCalledTimes(1);
380
+ });
381
+
382
+ it('does not delete stored state when reading MMKV fails', () => {
383
+ const store = {
384
+ getString: jest.fn(() => {
385
+ throw new Error('read failed');
386
+ }),
387
+ remove: jest.fn(),
388
+ delete: jest.fn(),
389
+ };
390
+ const { MmkvPersistentSessionsManager } = loadWithMmkvMock({
391
+ createMMKV: jest.fn().mockReturnValue(store),
392
+ });
393
+
394
+ expect(MmkvPersistentSessionsManager.fetchUserSession()).toBeNull();
395
+ expect(store.remove).not.toHaveBeenCalled();
396
+ expect(store.delete).not.toHaveBeenCalled();
397
+ });
398
+
399
+ it('updates activity in memory immediately and coalesces MMKV writes', async () => {
400
+ jest.useFakeTimers();
401
+ jest.setSystemTime(new Date('2026-08-19T12:00:00.000Z'));
402
+
403
+ try {
404
+ const store = {
405
+ getString: jest.fn().mockReturnValue(undefined),
406
+ set: jest.fn(),
407
+ };
408
+ jest.doMock('react-native-mmkv', () => ({ createMMKV: jest.fn().mockReturnValue(store) }));
409
+
410
+ const { mockConfig, MockTransport } = require('@grafana/faro-test-utils');
411
+ const { initializeFaro } = require('../../../initialize');
412
+ const { EVENT_NAVIGATION } = require('../../../navigation/utils');
413
+ const { SessionInstrumentation } = require('../index');
414
+ const { MmkvPersistentSessionsManager } = require('./MmkvPersistentSessionsManager');
415
+ const faro = await initializeFaro(
416
+ mockConfig({
417
+ url: 'http://localhost:12345/collect',
418
+ transports: [new MockTransport()],
419
+ instrumentations: [new SessionInstrumentation()],
420
+ sessionTracking: { enabled: true, persistent: true },
421
+ })
422
+ );
423
+ const instrumentation = faro.instrumentations.instrumentations.find(
424
+ ({ name }: { name: string }) => name === '@grafana/faro-react-native:instrumentation-session'
425
+ );
426
+ if (instrumentation == null || typeof instrumentation.unpatch !== 'function') {
427
+ throw new Error('Expected the default session instrumentation.');
428
+ }
429
+ const writesAfterInitialization = store.set.mock.calls.length;
430
+
431
+ jest.advanceTimersByTime(1);
432
+ faro.api.pushEvent(EVENT_NAVIGATION, { screen: 'cart' });
433
+ const activityAfterFirstEvent = MmkvPersistentSessionsManager.fetchUserSession().lastActivity;
434
+ jest.advanceTimersByTime(1);
435
+ faro.api.pushEvent(EVENT_NAVIGATION, { screen: 'checkout' });
436
+
437
+ expect(activityAfterFirstEvent).toBe(Date.now() - 1);
438
+ expect(MmkvPersistentSessionsManager.fetchUserSession().lastActivity).toBe(Date.now());
439
+ expect(store.set).toHaveBeenCalledTimes(writesAfterInitialization + 1);
440
+
441
+ jest.advanceTimersByTime(999);
442
+ expect(store.set).toHaveBeenCalledTimes(writesAfterInitialization + 2);
443
+
444
+ faro.api.pushEvent(EVENT_NAVIGATION, { screen: 'confirmation' });
445
+ instrumentation.unpatch();
446
+ expect(store.set).toHaveBeenCalledTimes(writesAfterInitialization + 3);
447
+
448
+ jest.advanceTimersByTime(1000);
449
+ expect(store.set).toHaveBeenCalledTimes(writesAfterInitialization + 3);
450
+ } finally {
451
+ jest.useRealTimers();
452
+ }
453
+ });
454
+
455
+ it('flushes pending activity when the app enters the background', async () => {
456
+ jest.useFakeTimers();
457
+ jest.setSystemTime(new Date('2026-08-19T12:00:00.000Z'));
458
+
459
+ try {
460
+ const store = {
461
+ getString: jest.fn().mockReturnValue(undefined),
462
+ set: jest.fn(),
463
+ };
464
+ jest.doMock('react-native-mmkv', () => ({ createMMKV: jest.fn().mockReturnValue(store) }));
465
+
466
+ const { mockConfig } = require('@grafana/faro-test-utils');
467
+ const { initializeFaro } = require('../../../initialize');
468
+ const { SessionActivityKind } = require('../sessionActivity');
469
+ const { MmkvPersistentSessionsManager } = require('./MmkvPersistentSessionsManager');
470
+ await initializeFaro(
471
+ mockConfig({
472
+ url: 'http://localhost:12345/collect',
473
+ instrumentations: [],
474
+ sessionTracking: { enabled: true, persistent: true },
475
+ })
476
+ );
477
+ const manager = new MmkvPersistentSessionsManager();
478
+ MmkvPersistentSessionsManager.storeUserSession({
479
+ sessionId: 'current-session',
480
+ started: Date.now() - 1000,
481
+ lastActivity: Date.now() - 1000,
482
+ isSampled: true,
483
+ });
484
+ const writesAfterStore = store.set.mock.calls.length;
485
+
486
+ manager.checkSession(SessionActivityKind.Meaningful);
487
+ jest.advanceTimersByTime(1);
488
+ manager.checkSession(SessionActivityKind.Meaningful);
489
+ expect(store.set).toHaveBeenCalledTimes(writesAfterStore + 1);
490
+
491
+ (
492
+ manager as unknown as {
493
+ handleAppStateChange: (state: 'background') => void;
494
+ }
495
+ ).handleAppStateChange('background');
496
+ expect(store.set).toHaveBeenCalledTimes(writesAfterStore + 2);
497
+
498
+ manager.unpatch();
499
+ } finally {
500
+ jest.useRealTimers();
501
+ }
502
+ });
503
+
504
+ it('rotates the persistent session at the inactivity boundary before attribution', async () => {
505
+ jest.useFakeTimers();
506
+ jest.setSystemTime(new Date('2026-08-19T12:00:00.000Z'));
507
+
508
+ try {
509
+ const store = {
510
+ getString: jest.fn().mockReturnValue(undefined),
511
+ set: jest.fn(),
512
+ };
513
+ jest.doMock('react-native-mmkv', () => ({ createMMKV: jest.fn().mockReturnValue(store) }));
514
+
515
+ const { TransportItemType } = require('@grafana/faro-core');
516
+ const { mockConfig, MockTransport } = require('@grafana/faro-test-utils');
517
+ const { initializeFaro } = require('../../../initialize');
518
+ const { SessionInstrumentation } = require('../index');
519
+ const { MmkvPersistentSessionsManager } = require('./MmkvPersistentSessionsManager');
520
+ const transport = new MockTransport();
521
+ const faro = await initializeFaro(
522
+ mockConfig({
523
+ url: 'http://localhost:12345/collect',
524
+ transports: [transport],
525
+ instrumentations: [new SessionInstrumentation()],
526
+ sessionTracking: { enabled: true, persistent: true },
527
+ })
528
+ );
529
+ const previousSession = MmkvPersistentSessionsManager.fetchUserSession();
530
+ if (previousSession == null) {
531
+ throw new Error('Expected an active persistent session.');
532
+ }
533
+ MmkvPersistentSessionsManager.storeUserSession({
534
+ ...previousSession,
535
+ lastActivity: Date.now() - 15 * 60 * 1000,
536
+ });
537
+
538
+ faro.api.pushEvent('poll_complete');
539
+
540
+ const nextSession = MmkvPersistentSessionsManager.fetchUserSession();
541
+ const sentEvent = transport.items.find(
542
+ (item: { type: string; payload: { name?: string } }) =>
543
+ item.type === TransportItemType.EVENT && item.payload.name === 'poll_complete'
544
+ );
545
+ expect(nextSession?.sessionId).not.toBe(previousSession.sessionId);
546
+ expect(nextSession?.sessionMeta?.attributes?.['previousSession']).toBe(previousSession.sessionId);
547
+ expect(sentEvent?.meta.session?.id).toBe(nextSession?.sessionId);
548
+ } finally {
549
+ jest.useRealTimers();
550
+ }
551
+ });
552
+ });