@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
@@ -0,0 +1,65 @@
1
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
2
+
3
+ import { initializeFaro } from '../../initialize';
4
+
5
+ import { SessionInstrumentation } from './index';
6
+ import { SessionActivityKind } from './sessionActivity';
7
+ import { VolatileSessionsManager } from './sessionManager/VolatileSessionManager';
8
+
9
+ describe('SessionInstrumentation session manager', () => {
10
+ beforeEach(() => {
11
+ jest.restoreAllMocks();
12
+ jest.clearAllMocks();
13
+ });
14
+
15
+ it('constructs a single session manager', async () => {
16
+ // Constructing a manager subscribes to AppState and to metas, and only the
17
+ // instance held by the instrumentation can be unpatched. A second, discarded
18
+ // instance would leak both subscriptions for the lifetime of the app.
19
+ const initSpy = jest.spyOn(VolatileSessionsManager.prototype as never, 'init' as never);
20
+
21
+ await initializeFaro(
22
+ mockConfig({
23
+ url: 'http://localhost:12345/collect',
24
+ transports: [new MockTransport()],
25
+ instrumentations: [new SessionInstrumentation()],
26
+ sessionTracking: {
27
+ enabled: true,
28
+ persistent: false,
29
+ },
30
+ })
31
+ );
32
+
33
+ expect(initSpy).toHaveBeenCalledTimes(1);
34
+ });
35
+
36
+ it('refreshes activity only after returning from the background', async () => {
37
+ await initializeFaro(
38
+ mockConfig({
39
+ url: 'http://localhost:12345/collect',
40
+ transports: [new MockTransport()],
41
+ instrumentations: [],
42
+ sessionTracking: { enabled: true, persistent: false },
43
+ })
44
+ );
45
+ const manager = new VolatileSessionsManager();
46
+ const checkSessionSpy = jest.spyOn(manager, 'checkSession');
47
+ const handleAppStateChange = (
48
+ manager as unknown as { handleAppStateChange: (state: 'active' | 'background' | 'inactive') => void }
49
+ ).handleAppStateChange;
50
+
51
+ handleAppStateChange('active');
52
+ checkSessionSpy.mockClear();
53
+ handleAppStateChange('inactive');
54
+ handleAppStateChange('active');
55
+ expect(checkSessionSpy).not.toHaveBeenCalled();
56
+
57
+ handleAppStateChange('background');
58
+ handleAppStateChange('inactive');
59
+ handleAppStateChange('active');
60
+ expect(checkSessionSpy).toHaveBeenCalledTimes(1);
61
+ expect(checkSessionSpy).toHaveBeenCalledWith(SessionActivityKind.Meaningful);
62
+
63
+ manager.unpatch();
64
+ });
65
+ });
@@ -0,0 +1,136 @@
1
+ import { NativeModules } from 'react-native';
2
+
3
+ import {
4
+ claimSessionPersistenceStorageId,
5
+ getSessionProcessInfo,
6
+ MAIN_PROCESS_SESSION_STORAGE_ID,
7
+ releaseSessionPersistenceOwnership,
8
+ resetSessionProcessForTests,
9
+ } from './sessionProcess';
10
+
11
+ const originalNativeModule = NativeModules.FaroReactNativeModule;
12
+
13
+ function setNativeModule(overrides: Record<string, unknown> = {}): {
14
+ claimSessionPersistence: jest.Mock;
15
+ getSessionProcessIdentifier: jest.Mock;
16
+ isMainSessionProcess: jest.Mock;
17
+ releaseSessionPersistence: jest.Mock;
18
+ } {
19
+ const nativeModule = {
20
+ claimSessionPersistence: jest.fn(() => true),
21
+ getSessionProcessIdentifier: jest.fn(() => 'com.example.myapp'),
22
+ isMainSessionProcess: jest.fn(() => true),
23
+ releaseSessionPersistence: jest.fn(() => true),
24
+ ...overrides,
25
+ };
26
+ NativeModules.FaroReactNativeModule = nativeModule;
27
+ return nativeModule;
28
+ }
29
+
30
+ describe('sessionProcess', () => {
31
+ beforeEach(() => {
32
+ resetSessionProcessForTests();
33
+ setNativeModule();
34
+ });
35
+
36
+ afterAll(() => {
37
+ NativeModules.FaroReactNativeModule = originalNativeModule;
38
+ resetSessionProcessForTests();
39
+ });
40
+
41
+ it('keeps the legacy MMKV id for the main process', () => {
42
+ const nativeModule = setNativeModule();
43
+
44
+ expect(claimSessionPersistenceStorageId()).toBe(MAIN_PROCESS_SESSION_STORAGE_ID);
45
+ expect(claimSessionPersistenceStorageId()).toBe(MAIN_PROCESS_SESSION_STORAGE_ID);
46
+ expect(nativeModule.claimSessionPersistence).toHaveBeenCalledTimes(1);
47
+ });
48
+
49
+ it('uses a separate stable MMKV id for a secondary process', () => {
50
+ setNativeModule({
51
+ getSessionProcessIdentifier: jest.fn(() => 'com.example.myapp:sync worker'),
52
+ isMainSessionProcess: jest.fn(() => false),
53
+ });
54
+
55
+ expect(claimSessionPersistenceStorageId()).toBe(
56
+ `${MAIN_PROCESS_SESSION_STORAGE_ID}.com.example.myapp%3Async%20worker`
57
+ );
58
+ expect(getSessionProcessInfo()).toEqual({
59
+ identifier: 'com.example.myapp:sync worker',
60
+ isMain: false,
61
+ });
62
+ });
63
+
64
+ it('caches the native process identity for this JavaScript runtime', () => {
65
+ const nativeModule = setNativeModule();
66
+
67
+ expect(getSessionProcessInfo()).toEqual({ identifier: 'com.example.myapp', isMain: true });
68
+ expect(getSessionProcessInfo()).toEqual({ identifier: 'com.example.myapp', isMain: true });
69
+ expect(nativeModule.getSessionProcessIdentifier).toHaveBeenCalledTimes(1);
70
+ expect(nativeModule.isMainSessionProcess).toHaveBeenCalledTimes(1);
71
+ });
72
+
73
+ it('fails closed when the native process bridge is unavailable', () => {
74
+ NativeModules.FaroReactNativeModule = undefined;
75
+ resetSessionProcessForTests();
76
+
77
+ expect(getSessionProcessInfo()).toBeNull();
78
+ expect(claimSessionPersistenceStorageId()).toBeNull();
79
+ });
80
+
81
+ it.each([
82
+ ['empty identity', { getSessionProcessIdentifier: jest.fn(() => ' ') }],
83
+ ['invalid main-process result', { isMainSessionProcess: jest.fn(() => null) }],
84
+ [
85
+ 'identity lookup failure',
86
+ {
87
+ getSessionProcessIdentifier: jest.fn(() => {
88
+ throw new Error('unavailable');
89
+ }),
90
+ },
91
+ ],
92
+ ])('fails closed for %s', (_name, overrides) => {
93
+ const nativeModule = setNativeModule(overrides);
94
+
95
+ expect(getSessionProcessInfo()).toBeNull();
96
+ expect(claimSessionPersistenceStorageId()).toBeNull();
97
+ expect(nativeModule.claimSessionPersistence).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('falls back to in-memory sessions when another runtime owns persistence', () => {
101
+ const nativeModule = setNativeModule({ claimSessionPersistence: jest.fn(() => false) });
102
+
103
+ expect(claimSessionPersistenceStorageId()).toBeNull();
104
+ expect(claimSessionPersistenceStorageId()).toBeNull();
105
+ expect(nativeModule.claimSessionPersistence).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ it('releases ownership so MMKV initialization can be retried safely', () => {
109
+ const nativeModule = setNativeModule();
110
+
111
+ expect(claimSessionPersistenceStorageId()).toBe(MAIN_PROCESS_SESSION_STORAGE_ID);
112
+ expect(releaseSessionPersistenceOwnership()).toBe(true);
113
+ expect(claimSessionPersistenceStorageId()).toBe(MAIN_PROCESS_SESSION_STORAGE_ID);
114
+
115
+ expect(nativeModule.claimSessionPersistence).toHaveBeenCalledTimes(2);
116
+ expect(nativeModule.releaseSessionPersistence).toHaveBeenCalledTimes(1);
117
+ });
118
+
119
+ it('does not release persistence when this runtime never claimed it', () => {
120
+ const nativeModule = setNativeModule({ claimSessionPersistence: jest.fn(() => false) });
121
+
122
+ expect(claimSessionPersistenceStorageId()).toBeNull();
123
+ expect(releaseSessionPersistenceOwnership()).toBe(false);
124
+ expect(nativeModule.releaseSessionPersistence).not.toHaveBeenCalled();
125
+ });
126
+
127
+ it('fails closed when the persistence claim throws', () => {
128
+ setNativeModule({
129
+ claimSessionPersistence: jest.fn(() => {
130
+ throw new Error('native failure');
131
+ }),
132
+ });
133
+
134
+ expect(claimSessionPersistenceStorageId()).toBeNull();
135
+ });
136
+ });
@@ -0,0 +1,114 @@
1
+ import { NativeModules } from 'react-native';
2
+
3
+ export const MAIN_PROCESS_SESSION_STORAGE_ID = 'grafana-faro-react-native-session';
4
+
5
+ interface SessionProcessNativeModule {
6
+ claimSessionPersistence?: () => unknown;
7
+ getSessionProcessIdentifier?: () => unknown;
8
+ isMainSessionProcess?: () => unknown;
9
+ releaseSessionPersistence?: () => unknown;
10
+ }
11
+
12
+ export interface SessionProcessInfo {
13
+ identifier: string;
14
+ isMain: boolean;
15
+ }
16
+
17
+ let cachedProcessInfo: SessionProcessInfo | null | undefined;
18
+ let cachedPersistenceOwnership: boolean | undefined;
19
+
20
+ function getNativeModule(): SessionProcessNativeModule | undefined {
21
+ return NativeModules['FaroReactNativeModule'] as SessionProcessNativeModule | undefined;
22
+ }
23
+
24
+ /** Stable native process identity used to isolate session chains. */
25
+ export function getSessionProcessInfo(): SessionProcessInfo | null {
26
+ if (cachedProcessInfo !== undefined) {
27
+ return cachedProcessInfo;
28
+ }
29
+
30
+ const nativeModule = getNativeModule();
31
+ if (
32
+ typeof nativeModule?.getSessionProcessIdentifier !== 'function' ||
33
+ typeof nativeModule.isMainSessionProcess !== 'function'
34
+ ) {
35
+ cachedProcessInfo = null;
36
+ return cachedProcessInfo;
37
+ }
38
+
39
+ try {
40
+ const identifier = nativeModule.getSessionProcessIdentifier();
41
+ const isMain = nativeModule.isMainSessionProcess();
42
+ if (typeof identifier !== 'string' || identifier.trim().length === 0 || typeof isMain !== 'boolean') {
43
+ cachedProcessInfo = null;
44
+ return cachedProcessInfo;
45
+ }
46
+
47
+ cachedProcessInfo = { identifier: identifier.trim(), isMain };
48
+ } catch {
49
+ cachedProcessInfo = null;
50
+ }
51
+
52
+ return cachedProcessInfo;
53
+ }
54
+
55
+ /**
56
+ * Returns the MMKV instance ID owned by this runtime, or null when exclusive
57
+ * ownership cannot be established. The main process retains the legacy ID so
58
+ * upgrades keep their existing previous-session link.
59
+ */
60
+ export function claimSessionPersistenceStorageId(): string | null {
61
+ const processInfo = getSessionProcessInfo();
62
+ if (processInfo == null) {
63
+ return null;
64
+ }
65
+
66
+ if (cachedPersistenceOwnership === undefined) {
67
+ const nativeModule = getNativeModule();
68
+ if (typeof nativeModule?.claimSessionPersistence !== 'function') {
69
+ cachedPersistenceOwnership = false;
70
+ } else {
71
+ try {
72
+ cachedPersistenceOwnership = nativeModule.claimSessionPersistence() === true;
73
+ } catch {
74
+ cachedPersistenceOwnership = false;
75
+ }
76
+ }
77
+ }
78
+
79
+ if (!cachedPersistenceOwnership) {
80
+ return null;
81
+ }
82
+
83
+ return processInfo.isMain
84
+ ? MAIN_PROCESS_SESSION_STORAGE_ID
85
+ : `${MAIN_PROCESS_SESSION_STORAGE_ID}.${encodeURIComponent(processInfo.identifier)}`;
86
+ }
87
+
88
+ /** Releases storage ownership after MMKV initialization fails. */
89
+ export function releaseSessionPersistenceOwnership(): boolean {
90
+ if (cachedPersistenceOwnership !== true) {
91
+ return false;
92
+ }
93
+
94
+ const nativeModule = getNativeModule();
95
+ if (typeof nativeModule?.releaseSessionPersistence !== 'function') {
96
+ return false;
97
+ }
98
+
99
+ try {
100
+ const released = nativeModule.releaseSessionPersistence() === true;
101
+ if (released) {
102
+ cachedPersistenceOwnership = undefined;
103
+ }
104
+ return released;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /** @internal */
111
+ export function resetSessionProcessForTests(): void {
112
+ cachedProcessInfo = undefined;
113
+ cachedPersistenceOwnership = undefined;
114
+ }
@@ -0,0 +1,337 @@
1
+ import { NativeModules } from 'react-native';
2
+
3
+ import { BaseInstrumentation, EVENT_SESSION_START, initializeFaro, VERSION } from '@grafana/faro-core';
4
+ import type { Faro } from '@grafana/faro-core';
5
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
6
+
7
+ import { SamplingFunction } from '../../config/sampling';
8
+ import { EVENT_NAVIGATION } from '../../navigation/utils';
9
+
10
+ import { SessionInstrumentation, startNewSession } from './index';
11
+ import {
12
+ MmkvPersistentSessionsManager,
13
+ resetMmkvSingletonForTests,
14
+ } from './sessionManager/MmkvPersistentSessionsManager';
15
+ import { STORAGE_KEY } from './sessionManager/sessionConstants';
16
+ import { VolatileSessionsManager } from './sessionManager/VolatileSessionManager';
17
+ import { resetSessionProcessForTests } from './sessionProcess';
18
+
19
+ const mockMmkvValues = new Map<string, string>();
20
+ const mockMmkv = {
21
+ getString: jest.fn((key: string) => mockMmkvValues.get(key)),
22
+ set: jest.fn((key: string, value: string) => mockMmkvValues.set(key, value)),
23
+ remove: jest.fn((key: string) => mockMmkvValues.delete(key)),
24
+ };
25
+ const originalNativeModule = NativeModules.FaroReactNativeModule;
26
+
27
+ jest.mock('react-native-mmkv', () => ({
28
+ createMMKV: jest.fn(() => mockMmkv),
29
+ }));
30
+
31
+ const initialTime = new Date('2026-08-20T12:00:00.000Z').getTime();
32
+
33
+ type InitializeSessionOptions = {
34
+ maxSessionPersistenceTime?: number;
35
+ persistent: boolean;
36
+ sessionIds?: string[];
37
+ samplingRates?: number[];
38
+ onSessionChange?: jest.Mock;
39
+ };
40
+
41
+ function initializeSession({
42
+ maxSessionPersistenceTime,
43
+ persistent,
44
+ sessionIds = ['session-1', 'session-2', 'session-3'],
45
+ samplingRates = [1, 1, 1],
46
+ onSessionChange = jest.fn(),
47
+ }: InitializeSessionOptions) {
48
+ const remainingSessionIds = [...sessionIds];
49
+ const generateSessionId = jest.fn(() => remainingSessionIds.shift() ?? 'fallback-session');
50
+ const resolveSampling = jest.fn(() => samplingRates.shift() ?? 1);
51
+ const transport = new MockTransport();
52
+ const instrumentation = new SessionInstrumentation();
53
+ const faro = initializeFaro(
54
+ mockConfig({
55
+ transports: [transport],
56
+ instrumentations: [instrumentation],
57
+ sessionTracking: {
58
+ enabled: true,
59
+ maxSessionPersistenceTime,
60
+ persistent,
61
+ generateSessionId,
62
+ onSessionChange,
63
+ sampling: new SamplingFunction(resolveSampling),
64
+ },
65
+ })
66
+ );
67
+
68
+ return { faro, generateSessionId, instrumentation, onSessionChange, resolveSampling, transport };
69
+ }
70
+
71
+ class CompatibleSessionInstrumentation extends BaseInstrumentation {
72
+ readonly name = '@grafana/faro-react-native:instrumentation-session';
73
+ readonly version = VERSION;
74
+ readonly startNewSession = jest.fn();
75
+
76
+ initialize(): void {}
77
+ }
78
+
79
+ function sessionStartItems(transport: MockTransport) {
80
+ return transport.items.filter(
81
+ (item) => item.type === 'event' && 'name' in item.payload && item.payload.name === EVENT_SESSION_START
82
+ );
83
+ }
84
+
85
+ describe('startNewSession', () => {
86
+ const initializedFaros: Faro[] = [];
87
+
88
+ beforeAll(() => {
89
+ jest.useFakeTimers();
90
+ });
91
+
92
+ beforeEach(() => {
93
+ NativeModules.FaroReactNativeModule = {
94
+ claimSessionPersistence: () => true,
95
+ getSessionProcessIdentifier: () => 'com.example.myapp',
96
+ isMainSessionProcess: () => true,
97
+ releaseSessionPersistence: () => true,
98
+ };
99
+ jest.setSystemTime(initialTime);
100
+ jest.clearAllMocks();
101
+ mockMmkvValues.clear();
102
+ resetMmkvSingletonForTests();
103
+ resetSessionProcessForTests();
104
+ VolatileSessionsManager.removeUserSession();
105
+ });
106
+
107
+ afterEach(() => {
108
+ for (const faro of initializedFaros.splice(0)) {
109
+ for (const instrumentation of faro.instrumentations.instrumentations) {
110
+ instrumentation.unpatch?.();
111
+ }
112
+ }
113
+ delete (globalThis as Record<string, unknown>)['faro'];
114
+ jest.restoreAllMocks();
115
+ });
116
+
117
+ afterAll(() => {
118
+ NativeModules.FaroReactNativeModule = originalNativeModule;
119
+ resetSessionProcessForTests();
120
+ jest.useRealTimers();
121
+ });
122
+
123
+ it.each([
124
+ ['volatile', false],
125
+ ['persistent', true],
126
+ ])('starts and links a new %s session on every reset', (_mode, persistent) => {
127
+ const setup = initializeSession({ persistent });
128
+ initializedFaros.push(setup.faro);
129
+ const initialSession = setup.faro.api.getSession();
130
+ const sessionsAtActionEnd: Array<string | undefined> = [];
131
+ const activeAction = {
132
+ end: jest.fn(() => sessionsAtActionEnd.push(setup.faro.api.getSession()?.id)),
133
+ };
134
+ jest.spyOn(setup.faro.api, 'getActiveUserAction').mockReturnValue(activeAction as never);
135
+
136
+ jest.setSystemTime(initialTime + 60_000);
137
+ startNewSession();
138
+ const secondSession = setup.faro.api.getSession();
139
+ const secondStoredSession = persistent
140
+ ? MmkvPersistentSessionsManager.fetchUserSession()
141
+ : VolatileSessionsManager.fetchUserSession();
142
+
143
+ jest.setSystemTime(initialTime + 120_000);
144
+ startNewSession();
145
+ const thirdSession = setup.faro.api.getSession();
146
+ const thirdStoredSession = persistent
147
+ ? MmkvPersistentSessionsManager.fetchUserSession()
148
+ : VolatileSessionsManager.fetchUserSession();
149
+
150
+ expect(initialSession?.id).toBe('session-1');
151
+ expect(secondSession).toMatchObject({
152
+ id: 'session-2',
153
+ attributes: { previousSession: 'session-1' },
154
+ });
155
+ expect(secondStoredSession).toMatchObject({
156
+ sessionId: 'session-2',
157
+ started: initialTime + 60_000,
158
+ lastActivity: initialTime + 60_000,
159
+ });
160
+ expect(thirdSession).toMatchObject({
161
+ id: 'session-3',
162
+ attributes: { previousSession: 'session-2' },
163
+ });
164
+ expect(thirdStoredSession).toMatchObject({
165
+ sessionId: 'session-3',
166
+ started: initialTime + 120_000,
167
+ lastActivity: initialTime + 120_000,
168
+ });
169
+ expect(sessionStartItems(setup.transport).map((item) => item.meta.session?.id)).toStrictEqual([
170
+ 'session-1',
171
+ 'session-2',
172
+ 'session-3',
173
+ ]);
174
+ expect(setup.onSessionChange).toHaveBeenNthCalledWith(1, initialSession, secondSession);
175
+ expect(setup.onSessionChange).toHaveBeenNthCalledWith(2, secondSession, thirdSession);
176
+ expect(activeAction.end).toHaveBeenCalledTimes(2);
177
+ expect(sessionsAtActionEnd).toStrictEqual(['session-1', 'session-2']);
178
+
179
+ if (persistent) {
180
+ expect(JSON.parse(mockMmkvValues.get(STORAGE_KEY) ?? '')).toMatchObject({
181
+ currentSessionId: 'session-3',
182
+ previousSessionId: 'session-2',
183
+ startedAt: initialTime + 120_000,
184
+ lastActivityAt: initialTime + 120_000,
185
+ });
186
+ }
187
+ });
188
+
189
+ it('makes a fresh sampling decision for every reset', () => {
190
+ const setup = initializeSession({ persistent: false, samplingRates: [1, 0, 1] });
191
+ initializedFaros.push(setup.faro);
192
+
193
+ expect(VolatileSessionsManager.fetchUserSession()?.isSampled).toBe(true);
194
+ startNewSession();
195
+ expect(VolatileSessionsManager.fetchUserSession()?.isSampled).toBe(false);
196
+ startNewSession();
197
+ expect(VolatileSessionsManager.fetchUserSession()?.isSampled).toBe(true);
198
+ expect(setup.resolveSampling).toHaveBeenCalledTimes(3);
199
+ expect(sessionStartItems(setup.transport).map((item) => item.meta.session?.id)).toStrictEqual([
200
+ 'session-1',
201
+ 'session-3',
202
+ ]);
203
+ });
204
+
205
+ it('uses compact fallback IDs when a custom generator repeats the current ID', () => {
206
+ const warn = jest.spyOn(console, 'warn').mockImplementation();
207
+ const setup = initializeSession({
208
+ persistent: false,
209
+ sessionIds: ['same-session', 'same-session', 'same-session', 'same-session'],
210
+ });
211
+ initializedFaros.push(setup.faro);
212
+
213
+ startNewSession();
214
+ const secondSessionId = setup.faro.api.getSession()?.id;
215
+ startNewSession();
216
+ const thirdSessionId = setup.faro.api.getSession()?.id;
217
+ startNewSession();
218
+ const fourthSessionId = setup.faro.api.getSession()?.id;
219
+
220
+ expect(secondSessionId).not.toBe('same-session');
221
+ expect(secondSessionId).not.toMatch(/^same-session-/);
222
+ expect(thirdSessionId).toBe('same-session');
223
+ expect(fourthSessionId).not.toBe('same-session');
224
+ expect(fourthSessionId).not.toMatch(/^same-session-/);
225
+ expect(setup.faro.api.getSession()?.attributes?.['previousSession']).toBe('same-session');
226
+ expect(warn).toHaveBeenCalledTimes(2);
227
+ expect(warn).toHaveBeenNthCalledWith(
228
+ 1,
229
+ 'The session ID generator returned the current session ID; using a generated fallback for the new session.'
230
+ );
231
+ });
232
+
233
+ it('keeps an active action with the previous session when an explicit reset happens after expiry', () => {
234
+ const setup = initializeSession({
235
+ maxSessionPersistenceTime: 15 * 60 * 1000,
236
+ persistent: false,
237
+ });
238
+ initializedFaros.push(setup.faro);
239
+ const activeAction = {
240
+ end: jest.fn(() => setup.faro.api.pushEvent('active_action_flushed')),
241
+ };
242
+ jest.spyOn(setup.faro.api, 'getActiveUserAction').mockReturnValue(activeAction as never);
243
+
244
+ jest.setSystemTime(initialTime + 15 * 60 * 1000);
245
+ startNewSession();
246
+
247
+ const flushedAction = setup.transport.items.find(
248
+ (item) => item.type === 'event' && 'name' in item.payload && item.payload.name === 'active_action_flushed'
249
+ );
250
+ expect(flushedAction?.meta.session?.id).toBe('session-1');
251
+ expect(setup.faro.api.getSession()).toMatchObject({
252
+ id: 'session-2',
253
+ attributes: { previousSession: 'session-1' },
254
+ });
255
+ expect(sessionStartItems(setup.transport).map((item) => item.meta.session?.id)).toStrictEqual([
256
+ 'session-1',
257
+ 'session-2',
258
+ ]);
259
+ expect(setup.generateSessionId).toHaveBeenCalledTimes(2);
260
+ expect(setup.onSessionChange).toHaveBeenCalledTimes(1);
261
+ });
262
+
263
+ it('clears a pending persistent activity write before storing the reset session', () => {
264
+ const setup = initializeSession({ persistent: true });
265
+ initializedFaros.push(setup.faro);
266
+
267
+ jest.advanceTimersByTime(1);
268
+ setup.faro.api.pushEvent(EVENT_NAVIGATION, { screen: 'cart' });
269
+ jest.advanceTimersByTime(1);
270
+ setup.faro.api.pushEvent(EVENT_NAVIGATION, { screen: 'checkout' });
271
+ const writesBeforeReset = mockMmkv.set.mock.calls.length;
272
+
273
+ jest.advanceTimersByTime(1);
274
+ startNewSession();
275
+ const writesAfterReset = mockMmkv.set.mock.calls.length;
276
+
277
+ expect(writesAfterReset).toBe(writesBeforeReset + 1);
278
+ expect(MmkvPersistentSessionsManager.fetchUserSession()?.sessionId).toBe('session-2');
279
+ jest.advanceTimersByTime(1000);
280
+ expect(mockMmkv.set).toHaveBeenCalledTimes(writesAfterReset);
281
+ });
282
+
283
+ it('uses a compatible session instrumentation from another module copy', () => {
284
+ const instrumentation = new CompatibleSessionInstrumentation();
285
+ const faro = initializeFaro(
286
+ mockConfig({
287
+ instrumentations: [instrumentation],
288
+ })
289
+ );
290
+ initializedFaros.push(faro);
291
+
292
+ startNewSession();
293
+
294
+ expect(instrumentation.startNewSession).toHaveBeenCalledTimes(1);
295
+ });
296
+
297
+ it('ignores a reset triggered re-entrantly by onSessionChange', () => {
298
+ const onSessionChange = jest.fn(() => startNewSession());
299
+ const setup = initializeSession({ persistent: false, onSessionChange });
300
+ initializedFaros.push(setup.faro);
301
+
302
+ startNewSession();
303
+
304
+ expect(setup.faro.api.getSession()?.id).toBe('session-2');
305
+ expect(setup.generateSessionId).toHaveBeenCalledTimes(2);
306
+ expect(onSessionChange).toHaveBeenCalledTimes(1);
307
+ });
308
+
309
+ it('does nothing when session tracking is disabled', () => {
310
+ const instrumentation = new SessionInstrumentation();
311
+ const transport = new MockTransport();
312
+ const faro = initializeFaro(
313
+ mockConfig({
314
+ transports: [transport],
315
+ instrumentations: [instrumentation],
316
+ sessionTracking: { enabled: false },
317
+ })
318
+ );
319
+ initializedFaros.push(faro);
320
+
321
+ startNewSession();
322
+
323
+ expect(faro.api.getSession()).toBeUndefined();
324
+ expect(sessionStartItems(transport)).toHaveLength(0);
325
+ });
326
+
327
+ it('does nothing after the session instrumentation is unpatched', () => {
328
+ const setup = initializeSession({ persistent: false });
329
+ initializedFaros.push(setup.faro);
330
+ setup.instrumentation.unpatch();
331
+
332
+ startNewSession();
333
+
334
+ expect(setup.faro.api.getSession()?.id).toBe('session-1');
335
+ expect(setup.generateSessionId).toHaveBeenCalledTimes(1);
336
+ });
337
+ });
@@ -13,15 +13,12 @@ const { FaroReactNativeModule } = NativeModules;
13
13
  * - iOS: sysctl() to query kernel for process start time
14
14
  * - Android: Process.getStartElapsedRealtime() from Android OS (API 24+)
15
15
  *
16
- * Implementation aligned with Faro Flutter SDK:
17
- * https://github.com/grafana/faro-flutter-sdk
18
- *
19
16
  * **Key Features**:
20
17
  * - ✅ NO AppDelegate/MainActivity setup required - OS tracks process start automatically!
21
18
  * - ✅ Cold start: appStartDuration from native, coldStart: 1
22
19
  * - ✅ Warm start: appStartDuration (time to first frame after resume), coldStart: 0
23
20
  *
24
- * **Metrics Captured** (matches Flutter SDK format):
21
+ * **Metrics Captured**:
25
22
  * - Cold start: `appStartDuration`, `coldStart: 1`
26
23
  * - Warm start: `appStartDuration`, `coldStart: 0`
27
24
  *
@@ -63,7 +60,7 @@ export class StartupInstrumentation extends BaseInstrumentation {
63
60
 
64
61
  /**
65
62
  * Captures cold start duration from native (process start to Faro init).
66
- * Matches Flutter SDK: appStartDuration + coldStart: 1
63
+ * Sends appStartDuration with coldStart: 1.
67
64
  */
68
65
  private captureColdStartMetrics(): void {
69
66
  try {
@@ -102,7 +99,6 @@ export class StartupInstrumentation extends BaseInstrumentation {
102
99
 
103
100
  /**
104
101
  * Tracks warm start: when app resumes from background, measures time to first frame.
105
- * Matches Flutter SDK: setWarmStart on resume, getWarmStart after postFrameCallback.
106
102
  */
107
103
  private setupWarmStartTracking(): void {
108
104
  this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => {