@grafana/faro-react-native 1.3.0 → 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 (266) hide show
  1. package/README.md +176 -30
  2. package/android/build.gradle +20 -15
  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/anr/instrumentation.d.ts +1 -1
  160. package/dist/types/instrumentations/appState/index.d.ts +1 -1
  161. package/dist/types/instrumentations/console/index.d.ts +1 -1
  162. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  163. package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +1 -1
  164. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  165. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +3 -3
  166. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +3 -3
  167. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  168. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +3 -3
  169. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  170. package/dist/types/instrumentations/http/index.d.ts +1 -1
  171. package/dist/types/instrumentations/performance/index.d.ts +1 -3
  172. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  173. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  174. package/dist/types/instrumentations/session/index.d.ts +21 -2
  175. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  176. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  177. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  178. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  179. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  180. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  181. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  182. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  183. package/dist/types/instrumentations/startup/index.d.ts +3 -7
  184. package/dist/types/instrumentations/userActions/index.d.ts +1 -1
  185. package/dist/types/instrumentations/view/index.d.ts +1 -1
  186. package/dist/types/instrumentations/xhr/index.d.ts +1 -1
  187. package/dist/types/navigation/utils.d.ts +1 -0
  188. package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
  189. package/dist/types/transports/console/transport.d.ts +1 -1
  190. package/dist/types/transports/fetch/index.d.ts +1 -1
  191. package/dist/types/transports/fetch/transport.d.ts +9 -2
  192. package/dist/types/transports/fetch/types.d.ts +5 -0
  193. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  194. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  195. package/dist/types/transports/offline/transport.d.ts +4 -5
  196. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  197. package/ios/FaroCrashReporter.swift +214 -21
  198. package/ios/FaroReactNative.swift +24 -0
  199. package/ios/FaroReactNativeModule.h +2 -1
  200. package/ios/FaroReactNativeModule.mm +121 -6
  201. package/ios/RefreshRateVitals.swift +34 -38
  202. package/package.json +8 -6
  203. package/src/config/getRNInstrumentations.ts +6 -8
  204. package/src/config/makeRNConfig.test.ts +6 -0
  205. package/src/config/sampling.ts +0 -3
  206. package/src/config/types.ts +5 -3
  207. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  208. package/src/generated/faroRNPackageMeta.ts +1 -1
  209. package/src/index.ts +8 -2
  210. package/src/instrumentations/anr/instrumentation.ts +8 -1
  211. package/src/instrumentations/appState/index.ts +10 -2
  212. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  213. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  214. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  215. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  216. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  217. package/src/instrumentations/crashReporting/types.ts +7 -0
  218. package/src/instrumentations/errors/index.ts +12 -4
  219. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  220. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  221. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  222. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  223. package/src/instrumentations/performance/index.ts +2 -6
  224. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  225. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  226. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  227. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  228. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  229. package/src/instrumentations/session/index.ts +269 -46
  230. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  231. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  232. package/src/instrumentations/session/sessionActivity.ts +58 -0
  233. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  234. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  235. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  236. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  237. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  238. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  239. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  240. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  241. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  242. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  243. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  244. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  245. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  246. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  247. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  248. package/src/instrumentations/session/sessionProcess.ts +114 -0
  249. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  250. package/src/instrumentations/startup/index.ts +2 -6
  251. package/src/navigation/utils.ts +3 -1
  252. package/src/transports/fetch/index.ts +6 -1
  253. package/src/transports/fetch/transport.test.ts +86 -2
  254. package/src/transports/fetch/transport.ts +38 -16
  255. package/src/transports/fetch/types.ts +7 -0
  256. package/src/transports/offline/ConnectivityService.ts +1 -1
  257. package/src/transports/offline/OfflineCache.ts +2 -2
  258. package/src/transports/offline/transport.ts +3 -4
  259. package/src/userPersistence/UserPersistence.ts +1 -2
  260. package/dist/cjs/utils/throttle.js +0 -42
  261. package/dist/cjs/utils/throttle.js.map +0 -1
  262. package/dist/esm/utils/throttle.js +0 -34
  263. package/dist/esm/utils/throttle.js.map +0 -1
  264. package/dist/types/utils/throttle.d.ts +0 -9
  265. package/src/utils/throttle.test.ts +0 -141
  266. package/src/utils/throttle.ts +0 -34
@@ -1,26 +1,135 @@
1
- import { BaseInstrumentation, dateNow, EVENT_SESSION_START, genShortID, VERSION } from '@grafana/faro-core';
2
- import type { Config, Meta, MetaSession, TransportItem } from '@grafana/faro-core';
1
+ import { BaseInstrumentation, dateNow, EVENT_SESSION_START, faro, VERSION } from '@grafana/faro-core';
2
+ import type {
3
+ API,
4
+ BeforeSendHook,
5
+ Config,
6
+ Meta,
7
+ Metas,
8
+ MetaSession,
9
+ TransportItem,
10
+ Transports,
11
+ UserActionInternalInterface,
12
+ } from '@grafana/faro-core';
3
13
 
4
14
  import type { ReactNativeFullConfig, ReactNativeSessionTrackingConfig } from '../../config/types';
5
15
 
16
+ import { clearDirectSessionActivityHandler, registerDirectSessionActivityHandler } from './directSessionActivity';
17
+ import { classifySessionActivity, isRecoveredCrashItem, SessionActivityKind } from './sessionActivity';
6
18
  import { minimalSessionDeviceAttributes, type SessionAttributes } from './sessionAttributes';
7
19
  import { type FaroUserSession, getSessionManagerByConfig, isSampled } from './sessionManager';
8
20
  import { MAX_SESSION_PERSISTENCE_TIME } from './sessionManager/sessionConstants';
9
21
  import { createUserSessionObject, isUserSessionValid } from './sessionManager/sessionManagerUtils';
10
- import type { SessionManager } from './sessionManager/types';
22
+ import type { SessionManager, SessionManagerInstance } from './sessionManager/types';
23
+
24
+ const SESSION_INSTRUMENTATION_NAME = '@grafana/faro-react-native:instrumentation-session';
25
+
26
+ // Track the current session hook per transport so superseded hooks yield to a
27
+ // replacement. The shared fallback preserves sampling decisions captured on
28
+ // queued items after the owning instrumentation has detached.
29
+ const activeSessionBeforeSendHooks = new WeakMap<Transports, BeforeSendHook>();
30
+ const sessionSamplingFallbackHooks = new WeakMap<Transports, BeforeSendHook>();
31
+
32
+ function applySessionSampling(item: TransportItem, fallbackSamplingDecision?: boolean): TransportItem | null {
33
+ const attributes = item.meta.session?.attributes;
34
+ const samplingAttribute = attributes?.['isSampled'] ?? fallbackSamplingDecision?.toString();
35
+
36
+ if (samplingAttribute === 'false') {
37
+ return null;
38
+ }
39
+
40
+ if (samplingAttribute !== 'true') {
41
+ return item;
42
+ }
43
+
44
+ const sampledItem: TransportItem = {
45
+ ...item,
46
+ meta: {
47
+ ...item.meta,
48
+ session: item.meta.session
49
+ ? {
50
+ ...item.meta.session,
51
+ attributes: item.meta.session.attributes ? { ...item.meta.session.attributes } : undefined,
52
+ }
53
+ : undefined,
54
+ },
55
+ };
56
+
57
+ const sampledAttributes = sampledItem.meta.session?.attributes;
58
+ delete sampledAttributes?.['isSampled'];
59
+
60
+ if (Object.keys(sampledAttributes ?? {}).length === 0) {
61
+ delete sampledItem.meta.session?.attributes;
62
+ }
63
+
64
+ return sampledItem;
65
+ }
66
+
67
+ function ensureSessionSamplingFallback(transports: Transports): void {
68
+ if (sessionSamplingFallbackHooks.has(transports)) {
69
+ return;
70
+ }
71
+
72
+ const fallbackHook: BeforeSendHook = (item) =>
73
+ activeSessionBeforeSendHooks.has(transports) ? item : applySessionSampling(item);
74
+
75
+ transports.addBeforeSendHooks(fallbackHook);
76
+ sessionSamplingFallbackHooks.set(transports, fallbackHook);
77
+ }
78
+
79
+ function removeInternalSamplingAttribute(session: MetaSession | undefined): MetaSession | undefined {
80
+ if (session?.attributes?.['isSampled'] == null) {
81
+ return session;
82
+ }
83
+
84
+ const attributes = { ...session.attributes };
85
+ delete attributes['isSampled'];
86
+
87
+ return {
88
+ ...session,
89
+ attributes: Object.keys(attributes).length === 0 ? undefined : attributes,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Starts a new linked session at an application-defined boundary.
95
+ *
96
+ * Call this after changing or clearing the current user for logout, account
97
+ * changes, or another boundary that must not share a session. Calls made
98
+ * before Faro initializes, after teardown, or while session tracking is
99
+ * disabled have no effect.
100
+ */
101
+ export function startNewSession(): void {
102
+ const instrumentation = faro?.instrumentations?.instrumentations.find(
103
+ ({ name }) => name === SESSION_INSTRUMENTATION_NAME
104
+ );
105
+
106
+ if (
107
+ instrumentation != null &&
108
+ 'startNewSession' in instrumentation &&
109
+ typeof instrumentation.startNewSession === 'function'
110
+ ) {
111
+ instrumentation.startNewSession();
112
+ }
113
+ }
11
114
 
12
115
  /**
13
116
  * Session instrumentation for React Native
14
117
  * Manages persistent or volatile sessions with expiration and inactivity tracking
15
118
  */
16
119
  export class SessionInstrumentation extends BaseInstrumentation {
17
- readonly name = '@grafana/faro-react-native:instrumentation-session';
120
+ readonly name = SESSION_INSTRUMENTATION_NAME;
18
121
  readonly version = VERSION;
19
122
 
20
123
  // previously notified session, to ensure we don't send session start
21
124
  // event twice for the same session
22
125
  private notifiedSession: MetaSession | undefined;
23
- private sessionManagerInstance: InstanceType<SessionManager> | undefined;
126
+ private sessionManagerInstance: SessionManagerInstance | undefined;
127
+ private isResettingSession = false;
128
+ private unregisterDirectSessionActivity: (() => void) | undefined;
129
+ private sessionMetaListenerMetas: Metas | undefined;
130
+ private beforeSendHookApi: API | undefined;
131
+ private beforeSendHook: BeforeSendHook | undefined;
132
+ private beforeSendHookTransports: Transports | undefined;
24
133
 
25
134
  private getDefaultSessionDeviceAttributes(): SessionAttributes {
26
135
  const cfg = this.config as ReactNativeFullConfig;
@@ -30,7 +139,7 @@ export class SessionInstrumentation extends BaseInstrumentation {
30
139
  return minimalSessionDeviceAttributes();
31
140
  }
32
141
 
33
- private sendSessionStartEvent(meta: Meta): void {
142
+ private readonly sendSessionStartEvent = (meta: Meta): void => {
34
143
  const session = meta.session;
35
144
 
36
145
  if (session && session.id !== this.notifiedSession?.id) {
@@ -39,6 +148,11 @@ export class SessionInstrumentation extends BaseInstrumentation {
39
148
  // automatically
40
149
  this.api.pushEvent(EVENT_SESSION_START, {}, undefined, { skipDedupe: true });
41
150
  }
151
+ };
152
+
153
+ private registerSessionMetaListener(): void {
154
+ this.metas.addListener(this.sendSessionStartEvent);
155
+ this.sessionMetaListenerMetas = this.metas;
42
156
  }
43
157
 
44
158
  private createInitialSession(
@@ -55,7 +169,10 @@ export class SessionInstrumentation extends BaseInstrumentation {
55
169
 
56
170
  if (sessionsConfig.persistent && storedUserSession) {
57
171
  const now = dateNow();
58
- const shouldClearPersistentSession = storedUserSession.lastActivity < now - maxPersistenceMs;
172
+ const shouldClearPersistentSession =
173
+ storedUserSession.started > now ||
174
+ storedUserSession.lastActivity > now ||
175
+ storedUserSession.lastActivity <= now - maxPersistenceMs;
59
176
 
60
177
  if (shouldClearPersistentSession) {
61
178
  SessionManagerClass.removeUserSession();
@@ -68,7 +185,7 @@ export class SessionInstrumentation extends BaseInstrumentation {
68
185
  let emitSessionStartOnInit: boolean;
69
186
  let initialSession: FaroUserSession;
70
187
 
71
- if (isUserSessionValid(storedUserSession)) {
188
+ if (!sessionsConfig.persistent && isUserSessionValid(storedUserSession)) {
72
189
  const sessionId = storedUserSession?.sessionId;
73
190
 
74
191
  initialSession = createUserSessionObject({
@@ -91,7 +208,7 @@ export class SessionInstrumentation extends BaseInstrumentation {
91
208
  ...sessionsConfig.session?.attributes,
92
209
  // Merge with stored attributes
93
210
  ...storedUserSessionMeta?.attributes,
94
- // Default attributes take precedence (matching Flutter SDK behavior)
211
+ // Default attributes take precedence
95
212
  ...defaultAttributes,
96
213
  // For valid resumed sessions we do not want to recalculate the sampling decision on each init phase.
97
214
  isSampled: initialSession.isSampled.toString(),
@@ -101,23 +218,24 @@ export class SessionInstrumentation extends BaseInstrumentation {
101
218
 
102
219
  emitSessionStartOnInit = false;
103
220
  } else {
104
- const sessionId = sessionsConfig.session?.id ?? genShortID();
105
-
106
221
  initialSession = createUserSessionObject({
107
- sessionId,
222
+ sessionId: sessionsConfig.session?.id,
108
223
  isSampled: isSampled(),
109
224
  });
110
225
 
226
+ const sessionId = initialSession.sessionId;
227
+ const previousSessionId = storedUserSession?.sessionId === sessionId ? undefined : storedUserSession?.sessionId;
111
228
  const overrides = sessionsConfig.session?.overrides;
112
229
 
113
230
  initialSession.sessionMeta = {
114
231
  id: sessionId,
115
232
  attributes: {
116
- isSampled: initialSession.isSampled.toString(),
117
233
  // Start with custom attributes from config
118
234
  ...sessionsConfig.session?.attributes,
119
- // Default attributes take precedence (matching Flutter SDK behavior)
235
+ // Default attributes take precedence
120
236
  ...defaultAttributes,
237
+ isSampled: initialSession.isSampled.toString(),
238
+ ...(previousSessionId == null ? {} : { previousSession: previousSessionId }),
121
239
  },
122
240
  // new session we don't care about previous overrides
123
241
  ...(overrides ? { overrides } : {}),
@@ -129,51 +247,80 @@ export class SessionInstrumentation extends BaseInstrumentation {
129
247
  return { initialSession, emitSessionStartOnInit };
130
248
  }
131
249
 
132
- private registerBeforeSendHook(SessionManagerClass: SessionManager) {
133
- const { updateSession } = new SessionManagerClass();
250
+ private registerBeforeSendHook(SessionManagerClass: SessionManager): void {
251
+ const registeredTransports = this.transports;
252
+ ensureSessionSamplingFallback(registeredTransports);
134
253
 
135
- this.transports?.addBeforeSendHooks((item: TransportItem) => {
136
- updateSession();
137
-
138
- const attributes = item.meta.session?.attributes;
139
-
140
- // Only filter out items when session is explicitly NOT sampled (isSampled='false')
141
- // If isSampled='true', remove the attribute before sending (it's internal)
142
- // If no isSampled attribute, pass through the item unchanged
143
- if (attributes?.['isSampled'] === 'false') {
144
- // Session is not sampled - drop this item
145
- return null;
254
+ const beforeSendHook: BeforeSendHook = (item: TransportItem) => {
255
+ const activeBeforeSendHook = activeSessionBeforeSendHooks.get(registeredTransports);
256
+ if (activeBeforeSendHook != null && activeBeforeSendHook !== beforeSendHook) {
257
+ return item;
146
258
  }
147
259
 
148
- if (attributes?.['isSampled'] === 'true') {
149
- // Session is sampled - remove internal isSampled attribute before sending
150
- let newItem: TransportItem = JSON.parse(JSON.stringify(item));
151
-
152
- const newAttributes = newItem.meta.session?.attributes;
153
- delete newAttributes?.['isSampled'];
154
-
155
- if (Object.keys(newAttributes ?? {}).length === 0) {
156
- delete newItem.meta.session?.attributes;
260
+ let nextItem = item;
261
+ let fallbackSamplingDecision: boolean | undefined;
262
+ const sessionManager = this.beforeSendHook === beforeSendHook ? this.sessionManagerInstance : undefined;
263
+
264
+ if (sessionManager != null) {
265
+ const storedSession = SessionManagerClass.fetchUserSession();
266
+ const recoveredCrash = isRecoveredCrashItem(item, storedSession?.sessionId);
267
+ // Ending an active action can send telemetry synchronously. Keep that
268
+ // telemetry on the current session until the explicit reset completes.
269
+ const checkedSession = recoveredCrash
270
+ ? null
271
+ : this.isResettingSession
272
+ ? storedSession
273
+ : sessionManager.checkSession(classifySessionActivity(item), storedSession);
274
+
275
+ if (checkedSession?.sessionMeta != null && item.meta.session?.id !== checkedSession.sessionId) {
276
+ nextItem = {
277
+ ...item,
278
+ meta: {
279
+ ...item.meta,
280
+ session: checkedSession.sessionMeta,
281
+ },
282
+ };
157
283
  }
158
284
 
159
- return newItem;
285
+ // New crash records carry the crash-time decision. Older records may not,
286
+ // so retain the previous behavior and use the live session's stable
287
+ // decision rather than implicitly sampling every recovered crash.
288
+ fallbackSamplingDecision = recoveredCrash ? storedSession?.isSampled : checkedSession?.isSampled;
160
289
  }
161
290
 
162
- // No isSampled attribute or other value - pass through unchanged
163
- return item;
164
- });
291
+ return applySessionSampling(nextItem, fallbackSamplingDecision);
292
+ };
293
+
294
+ this.beforeSendHook = beforeSendHook;
295
+ this.beforeSendHookApi = this.api;
296
+ this.beforeSendHookTransports = registeredTransports;
297
+ registeredTransports.addBeforeSendHooks(beforeSendHook);
298
+ activeSessionBeforeSendHooks.set(registeredTransports, beforeSendHook);
165
299
  }
166
300
 
167
301
  initialize(): void {
302
+ // BaseInstrumentation instances can be registered more than once. Detach
303
+ // every resource from the previous Faro instance before binding the next.
304
+ this.unpatch();
305
+
168
306
  const sessionTrackingConfig = this.config.sessionTracking;
169
307
 
308
+ // A new initialization owns direct-activity routing, including when
309
+ // session tracking is disabled.
310
+ clearDirectSessionActivityHandler();
311
+
170
312
  if (!sessionTrackingConfig?.enabled) {
171
- this.metas.addListener(this.sendSessionStartEvent.bind(this));
313
+ this.registerSessionMetaListener();
172
314
  return;
173
315
  }
174
316
 
175
317
  const SessionManagerClass = getSessionManagerByConfig(sessionTrackingConfig);
176
318
 
319
+ // Constructing a manager registers an AppState subscription and a metas
320
+ // listener, so exactly one instance is created and reused. It is also the
321
+ // instance `unpatch()` cleans up.
322
+ this.sessionManagerInstance = new SessionManagerClass(this.metas);
323
+
177
324
  this.registerBeforeSendHook(SessionManagerClass);
178
325
 
179
326
  const { initialSession, emitSessionStartOnInit } = this.createInitialSession(
@@ -186,21 +333,97 @@ export class SessionInstrumentation extends BaseInstrumentation {
186
333
  this.notifiedSession = initialSessionMeta;
187
334
  this.api.setSession(initialSessionMeta);
188
335
 
189
- this.sessionManagerInstance = new SessionManagerClass();
190
-
191
336
  if (emitSessionStartOnInit) {
192
337
  this.api.pushEvent(EVENT_SESSION_START, {}, undefined, { skipDedupe: true });
193
338
  }
194
339
 
195
- this.metas.addListener(this.sendSessionStartEvent.bind(this));
340
+ this.unregisterDirectSessionActivity = registerDirectSessionActivityHandler(() => {
341
+ this.sessionManagerInstance?.checkSession(SessionActivityKind.Meaningful);
342
+ });
343
+
344
+ this.registerSessionMetaListener();
345
+ }
346
+
347
+ /** Starts a new linked session immediately. */
348
+ startNewSession(): void {
349
+ if (this.sessionManagerInstance == null || this.isResettingSession) {
350
+ return;
351
+ }
352
+
353
+ this.isResettingSession = true;
354
+ try {
355
+ try {
356
+ const activeAction = this.api.getActiveUserAction?.() as UserActionInternalInterface | undefined;
357
+ activeAction?.end();
358
+ } catch (error) {
359
+ this.logWarn('Failed to end the active user action before resetting the session:', error);
360
+ }
361
+
362
+ this.sessionManagerInstance.resetSession();
363
+ } finally {
364
+ this.isResettingSession = false;
365
+ }
196
366
  }
197
367
 
198
368
  /**
199
369
  * Clean up session manager listeners
200
370
  */
201
371
  unpatch(): void {
202
- if (this.sessionManagerInstance && 'unpatch' in this.sessionManagerInstance) {
203
- this.sessionManagerInstance.unpatch();
372
+ this.unregisterDirectSessionActivity?.();
373
+ this.unregisterDirectSessionActivity = undefined;
374
+
375
+ const beforeSendHook = this.beforeSendHook;
376
+ const beforeSendHookApi = this.beforeSendHookApi;
377
+ const beforeSendHookTransports = this.beforeSendHookTransports;
378
+ const ownedActiveHook =
379
+ beforeSendHook != null &&
380
+ beforeSendHookTransports != null &&
381
+ activeSessionBeforeSendHooks.get(beforeSendHookTransports) === beforeSendHook;
382
+
383
+ // Stop the manager's metas synchronization before removing the internal
384
+ // sampling marker. Keep the session hook active during that update so any
385
+ // surviving metas listener that emits telemetry is still attributed and
386
+ // filtered with the current session.
387
+ this.sessionManagerInstance?.unpatch();
388
+
389
+ try {
390
+ if (ownedActiveHook && beforeSendHookApi) {
391
+ const session = beforeSendHookApi.getSession();
392
+ const sessionWithoutSamplingAttribute = removeInternalSamplingAttribute(session);
393
+ if (sessionWithoutSamplingAttribute !== session) {
394
+ this.isResettingSession = true;
395
+ try {
396
+ beforeSendHookApi.setSession(sessionWithoutSamplingAttribute);
397
+ } finally {
398
+ this.isResettingSession = false;
399
+ }
400
+ }
401
+ }
402
+ } finally {
403
+ this.beforeSendHook = undefined;
404
+ this.beforeSendHookApi = undefined;
405
+ this.beforeSendHookTransports = undefined;
406
+ if (beforeSendHook && beforeSendHookTransports) {
407
+ if (ownedActiveHook) {
408
+ activeSessionBeforeSendHooks.delete(beforeSendHookTransports);
409
+ }
410
+ beforeSendHookTransports.removeBeforeSendHooks(beforeSendHook);
411
+ }
412
+
413
+ this.sessionManagerInstance = undefined;
414
+
415
+ const sessionMetaListenerMetas = this.sessionMetaListenerMetas;
416
+ this.sessionMetaListenerMetas = undefined;
417
+ if (sessionMetaListenerMetas) {
418
+ sessionMetaListenerMetas.removeListener(this.sendSessionStartEvent);
419
+ }
420
+
421
+ this.notifiedSession = undefined;
422
+ this.isResettingSession = false;
204
423
  }
205
424
  }
425
+
426
+ destroy(): void {
427
+ this.unpatch();
428
+ }
206
429
  }
@@ -0,0 +1,196 @@
1
+ import { NativeModules } from 'react-native';
2
+
3
+ import { EVENT_SESSION_START, initializeFaro } from '@grafana/faro-core';
4
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
5
+
6
+ import { SessionInstrumentation } from './index';
7
+ import {
8
+ MmkvPersistentSessionsManager,
9
+ resetMmkvSingletonForTests,
10
+ } from './sessionManager/MmkvPersistentSessionsManager';
11
+ import { MAX_SESSION_PERSISTENCE_TIME, STORAGE_KEY } from './sessionManager/sessionConstants';
12
+ import { resetSessionProcessForTests } from './sessionProcess';
13
+
14
+ const mockMmkvValues = new Map<string, string>();
15
+ const mockMmkv = {
16
+ getString: jest.fn((key: string) => mockMmkvValues.get(key)),
17
+ set: jest.fn((key: string, value: string) => mockMmkvValues.set(key, value)),
18
+ remove: jest.fn((key: string) => mockMmkvValues.delete(key)),
19
+ };
20
+ const mockCreateMMKV = jest.fn(() => mockMmkv);
21
+ const originalNativeModule = NativeModules.FaroReactNativeModule;
22
+
23
+ jest.mock('react-native-mmkv', () => ({
24
+ createMMKV: mockCreateMMKV,
25
+ }));
26
+
27
+ const now = new Date('2026-08-14T12:00:00.000Z').getTime();
28
+
29
+ function persistedSession({
30
+ currentSessionId = 'persisted-session',
31
+ startedAt = now - 10_000,
32
+ lastActivityAt = now - 1,
33
+ }: {
34
+ currentSessionId?: string;
35
+ startedAt?: number;
36
+ lastActivityAt?: number;
37
+ } = {}) {
38
+ return JSON.stringify({
39
+ schemaVersion: 1,
40
+ currentSessionId,
41
+ previousSessionId: 'older-session',
42
+ startedAt,
43
+ lastActivityAt,
44
+ isSampled: false,
45
+ });
46
+ }
47
+
48
+ function initializePersistentSession() {
49
+ const transport = new MockTransport();
50
+ const faro = initializeFaro(
51
+ mockConfig({
52
+ transports: [transport],
53
+ instrumentations: [new SessionInstrumentation()],
54
+ sessionTracking: {
55
+ enabled: true,
56
+ persistent: true,
57
+ generateSessionId: () => 'new-session',
58
+ },
59
+ })
60
+ );
61
+
62
+ return { faro, transport };
63
+ }
64
+
65
+ describe('persistent session cold starts', () => {
66
+ beforeAll(() => {
67
+ jest.useFakeTimers();
68
+ jest.setSystemTime(now);
69
+ });
70
+
71
+ beforeEach(() => {
72
+ NativeModules.FaroReactNativeModule = {
73
+ claimSessionPersistence: () => true,
74
+ getSessionProcessIdentifier: () => 'com.example.myapp',
75
+ isMainSessionProcess: () => true,
76
+ releaseSessionPersistence: () => true,
77
+ };
78
+ jest.clearAllMocks();
79
+ mockMmkvValues.clear();
80
+ resetMmkvSingletonForTests();
81
+ resetSessionProcessForTests();
82
+ delete (globalThis as Record<string, unknown>)['faro'];
83
+ });
84
+
85
+ afterAll(() => {
86
+ NativeModules.FaroReactNativeModule = originalNativeModule;
87
+ resetSessionProcessForTests();
88
+ jest.useRealTimers();
89
+ });
90
+
91
+ it('starts an unlinked session when stored state is missing', () => {
92
+ const { faro } = initializePersistentSession();
93
+
94
+ expect(faro.metas.value.session?.id).toBe('new-session');
95
+ expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
96
+ });
97
+
98
+ it('creates a new session linked to a valid persisted session', () => {
99
+ mockMmkvValues.set(STORAGE_KEY, persistedSession());
100
+
101
+ const { faro, transport } = initializePersistentSession();
102
+
103
+ expect(faro.metas.value.session?.id).toBe('new-session');
104
+ expect(faro.metas.value.session?.attributes?.['previousSession']).toBe('persisted-session');
105
+ const sessionStartEvents = transport.items.filter(
106
+ (item) => item.type === 'event' && 'name' in item.payload && item.payload.name === EVENT_SESSION_START
107
+ );
108
+ expect(sessionStartEvents).toHaveLength(1);
109
+ expect(sessionStartEvents[0]?.meta.session?.attributes?.['previousSession']).toBe('persisted-session');
110
+ });
111
+
112
+ it('does not load MMKV when persistence is disabled', () => {
113
+ initializeFaro(
114
+ mockConfig({
115
+ transports: [new MockTransport()],
116
+ instrumentations: [new SessionInstrumentation()],
117
+ sessionTracking: {
118
+ enabled: true,
119
+ persistent: false,
120
+ },
121
+ })
122
+ );
123
+
124
+ expect(mockCreateMMKV).not.toHaveBeenCalled();
125
+ });
126
+
127
+ it('does not link expired persisted state', () => {
128
+ const lastActivityAt = now - MAX_SESSION_PERSISTENCE_TIME;
129
+ mockMmkvValues.set(STORAGE_KEY, persistedSession({ startedAt: lastActivityAt - 10_000, lastActivityAt }));
130
+
131
+ const { faro } = initializePersistentSession();
132
+
133
+ expect(faro.metas.value.session?.id).toBe('new-session');
134
+ expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
135
+ expect(mockMmkv.remove).toHaveBeenCalledWith(STORAGE_KEY);
136
+ });
137
+
138
+ it('does not link future-dated persisted state', () => {
139
+ mockMmkvValues.set(STORAGE_KEY, persistedSession({ startedAt: now + 1, lastActivityAt: now + 1 }));
140
+
141
+ const { faro } = initializePersistentSession();
142
+
143
+ expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
144
+ expect(mockMmkv.remove).toHaveBeenCalledWith(STORAGE_KEY);
145
+ });
146
+
147
+ it('does not link a configured session ID to itself', () => {
148
+ mockMmkvValues.set(STORAGE_KEY, persistedSession({ currentSessionId: 'new-session' }));
149
+
150
+ const { faro } = initializePersistentSession();
151
+
152
+ expect(faro.metas.value.session?.id).toBe('new-session');
153
+ expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
154
+ });
155
+
156
+ it.each([
157
+ ['corrupt', '{not-json'],
158
+ [
159
+ 'unsupported',
160
+ JSON.stringify({
161
+ schemaVersion: 99,
162
+ currentSessionId: 'persisted-session',
163
+ previousSessionId: null,
164
+ startedAt: now - 10_000,
165
+ lastActivityAt: now - 1,
166
+ isSampled: true,
167
+ }),
168
+ ],
169
+ ])('starts cleanly when persisted state is %s', (_name, value) => {
170
+ mockMmkvValues.set(STORAGE_KEY, value);
171
+
172
+ const { faro } = initializePersistentSession();
173
+
174
+ expect(faro.metas.value.session?.id).toBe('new-session');
175
+ expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
176
+ expect(mockMmkv.remove).toHaveBeenCalledWith(STORAGE_KEY);
177
+ });
178
+
179
+ it('stores only the new session and its link after initialization', () => {
180
+ mockMmkvValues.set(STORAGE_KEY, persistedSession());
181
+
182
+ initializePersistentSession();
183
+
184
+ const storedSession = mockMmkvValues.get(STORAGE_KEY);
185
+ expect(storedSession).toBeDefined();
186
+ expect(JSON.parse(storedSession ?? '')).toStrictEqual({
187
+ schemaVersion: 1,
188
+ currentSessionId: 'new-session',
189
+ previousSessionId: 'persisted-session',
190
+ startedAt: now,
191
+ lastActivityAt: now,
192
+ isSampled: true,
193
+ });
194
+ expect(MmkvPersistentSessionsManager.fetchUserSession()?.sessionId).toBe('new-session');
195
+ });
196
+ });
@@ -0,0 +1,68 @@
1
+ import { EVENT_VIEW_CHANGED, TransportItemType } from '@grafana/faro-core';
2
+ import type { TransportItem } from '@grafana/faro-core';
3
+
4
+ import { EVENT_NAVIGATION } from '../../navigation/utils';
5
+ import { EVENT_APP_STATE_CHANGED } from '../appState';
6
+
7
+ import { classifySessionActivity, isRecoveredCrashItem, SessionActivityKind } from './sessionActivity';
8
+
9
+ function item(type: TransportItemType, payload: Record<string, unknown>, sessionId = 'current'): TransportItem {
10
+ return {
11
+ type,
12
+ payload: payload as TransportItem['payload'],
13
+ meta: { session: { id: sessionId } },
14
+ };
15
+ }
16
+
17
+ describe('session activity classification', () => {
18
+ it.each([
19
+ ['view changes', item(TransportItemType.EVENT, { name: EVENT_VIEW_CHANGED })],
20
+ ['navigation', item(TransportItemType.EVENT, { name: EVENT_NAVIGATION })],
21
+ [
22
+ 'foreground returns',
23
+ item(TransportItemType.EVENT, {
24
+ name: EVENT_APP_STATE_CHANGED,
25
+ attributes: { fromState: 'background', toState: 'active' },
26
+ }),
27
+ ],
28
+ ['tracked actions', item(TransportItemType.EXCEPTION, { action: { name: 'checkout' } })],
29
+ ])('treats %s as meaningful', (_name, transportItem) => {
30
+ expect(classifySessionActivity(transportItem)).toBe(SessionActivityKind.Meaningful);
31
+ });
32
+
33
+ it.each([
34
+ ['generic events', item(TransportItemType.EVENT, { name: 'poll_complete' })],
35
+ ['unmarked HTTP events', item(TransportItemType.EVENT, { name: 'faro.tracing.fetch' })],
36
+ [
37
+ 'background lifecycle events',
38
+ item(TransportItemType.EVENT, {
39
+ name: EVENT_APP_STATE_CHANGED,
40
+ attributes: { toState: 'background' },
41
+ }),
42
+ ],
43
+ [
44
+ 'returns from an inactive overlay',
45
+ item(TransportItemType.EVENT, {
46
+ name: EVENT_APP_STATE_CHANGED,
47
+ attributes: { fromState: 'inactive', toState: 'active' },
48
+ }),
49
+ ],
50
+ ['logs', item(TransportItemType.LOG, { message: 'heartbeat' })],
51
+ ['errors', item(TransportItemType.EXCEPTION, { type: 'error', value: 'failure' })],
52
+ ['measurements', item(TransportItemType.MEASUREMENT, { type: 'cpu', values: { usage: 1 } })],
53
+ ['traces', item(TransportItemType.TRACE, { trace_id: 'trace-id' })],
54
+ ])('treats %s as passive', (_name, transportItem) => {
55
+ expect(classifySessionActivity(transportItem)).toBe(SessionActivityKind.Passive);
56
+ });
57
+
58
+ it('recognizes a fatal crash recovered from a previous session', () => {
59
+ const recoveredCrash = item(
60
+ TransportItemType.EXCEPTION,
61
+ { fatal: true, type: 'crash', value: 'native crash' },
62
+ 'previous'
63
+ );
64
+
65
+ expect(isRecoveredCrashItem(recoveredCrash, 'current')).toBe(true);
66
+ expect(isRecoveredCrashItem(recoveredCrash, 'previous')).toBe(false);
67
+ });
68
+ });