@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
@@ -0,0 +1,58 @@
1
+ import { EVENT_VIEW_CHANGED, TransportItemType } from '@grafana/faro-core';
2
+ import type { EventAttributes, TransportItem } from '@grafana/faro-core';
3
+
4
+ import { EVENT_NAVIGATION } from '../../navigation/utils';
5
+ import { EVENT_APP_STATE_CHANGED } from '../appState';
6
+
7
+ export enum SessionActivityKind {
8
+ Meaningful = 'meaningful',
9
+ Passive = 'passive',
10
+ }
11
+
12
+ type ActivityEventPayload = {
13
+ action?: unknown;
14
+ attributes?: EventAttributes;
15
+ name?: string;
16
+ };
17
+
18
+ /** Classifies how a transport item affects the session inactivity window. */
19
+ export function classifySessionActivity(item: TransportItem): SessionActivityKind {
20
+ const payload = item.payload as ActivityEventPayload;
21
+
22
+ if (payload.action != null) {
23
+ return SessionActivityKind.Meaningful;
24
+ }
25
+
26
+ if (item.type !== TransportItemType.EVENT) {
27
+ return SessionActivityKind.Passive;
28
+ }
29
+
30
+ if (payload.name === EVENT_VIEW_CHANGED || payload.name === EVENT_NAVIGATION) {
31
+ return SessionActivityKind.Meaningful;
32
+ }
33
+
34
+ if (
35
+ payload.name === EVENT_APP_STATE_CHANGED &&
36
+ payload.attributes?.['fromState'] === 'background' &&
37
+ payload.attributes?.['toState'] === 'active'
38
+ ) {
39
+ return SessionActivityKind.Meaningful;
40
+ }
41
+
42
+ return SessionActivityKind.Passive;
43
+ }
44
+
45
+ type CrashPayload = {
46
+ fatal?: unknown;
47
+ type?: unknown;
48
+ };
49
+
50
+ /** Recovered crashes keep the session captured by the previous process. */
51
+ export function isRecoveredCrashItem(item: TransportItem, currentSessionId: string | undefined): boolean {
52
+ if (item.type !== TransportItemType.EXCEPTION || item.meta.session?.id == null) {
53
+ return false;
54
+ }
55
+
56
+ const payload = item.payload as CrashPayload;
57
+ return payload.type === 'crash' && payload.fatal === true && item.meta.session.id !== currentSessionId;
58
+ }
@@ -1,11 +1,19 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage';
2
- import { Platform } from 'react-native';
2
+ import { NativeModules, Platform } from 'react-native';
3
3
  import DeviceInfo from 'react-native-device-info';
4
4
 
5
- import { getSessionAttributes, loadMobileMetaForInit } from './sessionAttributes';
5
+ import { getSessionAttributes, loadMobileMetaForInit, minimalSessionDeviceAttributes } from './sessionAttributes';
6
+ import { resetSessionProcessForTests } from './sessionProcess';
6
7
 
7
8
  const INSTALLATION_ID_STORAGE_KEY = '@grafana/faro-react-native/installation_id';
8
9
  const STORED_INSTALLATION_ID = 'stored-installation-id';
10
+ const originalNativeModule = NativeModules.FaroReactNativeModule;
11
+ const sessionNativeModule = {
12
+ claimSessionPersistence: () => true,
13
+ getSessionProcessIdentifier: () => 'com.example.myapp',
14
+ isMainSessionProcess: () => true,
15
+ releaseSessionPersistence: () => true,
16
+ };
9
17
 
10
18
  function setStoredInstallationId(value = STORED_INSTALLATION_ID): void {
11
19
  (global as any).mockAsyncStorage = {
@@ -37,7 +45,14 @@ jest.mock('react-native-device-info', () => ({
37
45
  describe('sessionAttributes', () => {
38
46
  beforeEach(() => {
39
47
  (global as any).mockAsyncStorage = {};
48
+ NativeModules.FaroReactNativeModule = sessionNativeModule;
40
49
  jest.clearAllMocks();
50
+ resetSessionProcessForTests();
51
+ });
52
+
53
+ afterAll(() => {
54
+ NativeModules.FaroReactNativeModule = originalNativeModule;
55
+ resetSessionProcessForTests();
41
56
  });
42
57
 
43
58
  describe('getSessionAttributes', () => {
@@ -79,6 +94,7 @@ describe('sessionAttributes', () => {
79
94
 
80
95
  expect(attributes).toEqual({
81
96
  react_native_version: '0.75.1',
97
+ process_name: 'com.example.myapp',
82
98
  device_os: 'iOS',
83
99
  device_os_version: '17.0',
84
100
  device_os_detail: 'iOS 17.0',
@@ -177,6 +193,7 @@ describe('sessionAttributes', () => {
177
193
 
178
194
  expect(attributes).toEqual({
179
195
  react_native_version: '0.75.1',
196
+ process_name: 'com.example.myapp',
180
197
  device_os: 'Android',
181
198
  device_os_version: '15',
182
199
  device_os_detail: 'Android 15 (SDK 35)',
@@ -439,6 +456,7 @@ describe('sessionAttributes', () => {
439
456
  expect(mobileMeta).toEqual({
440
457
  sessionAttributes: {
441
458
  react_native_version: '0.75.1',
459
+ process_name: 'com.example.myapp',
442
460
  },
443
461
  meta: {
444
462
  app: {
@@ -531,6 +549,7 @@ describe('sessionAttributes', () => {
531
549
  // Device info is omitted when collection fails so nothing partial is sent to Faro
532
550
  expect(attributes).toEqual({
533
551
  react_native_version: expect.any(String),
552
+ process_name: 'com.example.myapp',
534
553
  });
535
554
  expect(attributes.device_id).toBeUndefined();
536
555
  expect(attributes.device_os).toBeUndefined();
@@ -555,6 +574,7 @@ describe('sessionAttributes', () => {
555
574
  // Any synchronous DeviceInfo failure skips all device fields (minimal payload only)
556
575
  expect(attributes).toEqual({
557
576
  react_native_version: expect.any(String),
577
+ process_name: 'com.example.myapp',
558
578
  });
559
579
  expect(attributes.device_id).toBeUndefined();
560
580
  expect(attributes.device_manufacturer).toBeUndefined();
@@ -601,5 +621,20 @@ describe('sessionAttributes', () => {
601
621
  expect(attributes.device_manufacturer).toBe('oneplus');
602
622
  });
603
623
  });
624
+
625
+ it('omits process identity when the native bridge is unavailable', () => {
626
+ const nativeModule = NativeModules.FaroReactNativeModule;
627
+ NativeModules.FaroReactNativeModule = undefined;
628
+ resetSessionProcessForTests();
629
+
630
+ try {
631
+ expect(minimalSessionDeviceAttributes()).toEqual({
632
+ react_native_version: expect.any(String),
633
+ });
634
+ } finally {
635
+ NativeModules.FaroReactNativeModule = nativeModule;
636
+ resetSessionProcessForTests();
637
+ }
638
+ });
604
639
  });
605
640
  });
@@ -4,13 +4,14 @@ import DeviceInfo from 'react-native-device-info';
4
4
  import type { Meta } from '@grafana/faro-core';
5
5
 
6
6
  import { getInstallationId } from './installationId';
7
+ import { getSessionProcessInfo } from './sessionProcess';
7
8
 
8
9
  /**
9
10
  * Session attributes for React Native
10
11
  * These attributes are automatically included with every telemetry event
11
12
  *
12
- * Core attributes match Flutter SDK format, with additional mobile-specific
13
- * monitoring fields (memory, device type, battery, etc.)
13
+ * Core mobile session attributes, with additional monitoring fields
14
+ * (memory, device type, battery, etc.)
14
15
  *
15
16
  * SDK name, core version, and npm adapter are on Faro meta `sdk` (`getSdkMeta` in `metas/sdk.ts`).
16
17
  *
@@ -20,6 +21,9 @@ export interface SessionAttributes {
20
21
  /** Host app's React Native framework version (e.g. "0.75.1") from `Platform.constants`. */
21
22
  react_native_version: string;
22
23
 
24
+ /** Stable Android process name or Apple bundle identifier for this runtime. */
25
+ process_name?: string;
26
+
23
27
  /** Operating system ("iOS" or "Android") */
24
28
  device_os?: string;
25
29
 
@@ -138,7 +142,7 @@ function getMobileDeviceType(isTablet: boolean): 'mobile' | 'tablet' | undefined
138
142
  }
139
143
 
140
144
  /**
141
- * Get OS detail string matching Flutter SDK format
145
+ * Get OS detail string for session attributes.
142
146
  * iOS: "iOS 17.0"
143
147
  * Android: "Android 15 (SDK 35)"
144
148
  */
@@ -174,8 +178,10 @@ async function getDeviceOsBuildId(): Promise<string | undefined> {
174
178
  * No synchronous DeviceInfo reads — use {@link getSessionAttributes} or the package async `initializeFaro`.
175
179
  */
176
180
  export function minimalSessionDeviceAttributes(): SessionAttributes {
181
+ const processInfo = getSessionProcessInfo();
177
182
  return {
178
183
  react_native_version: getReactNativeVersion(),
184
+ ...(processInfo == null ? {} : { process_name: processInfo.identifier }),
179
185
  };
180
186
  }
181
187
 
@@ -183,8 +189,9 @@ export function minimalSessionDeviceAttributes(): SessionAttributes {
183
189
  * Get all session attributes
184
190
  * These attributes are automatically included with every telemetry event
185
191
  *
186
- * Core attributes matching Flutter SDK:
192
+ * Core session attributes:
187
193
  * - react_native_version (RN framework in the host app)
194
+ * - process_name (Android process name or iOS bundle identifier)
188
195
  * - device_os, device_os_version, device_os_detail
189
196
  * - device_manufacturer, device_model, device_model_name
190
197
  * - device_brand, device_is_physical, device_id
@@ -222,7 +229,6 @@ async function collectMobileMeta(): Promise<PreloadedMobileMeta> {
222
229
  // Memory info
223
230
  const totalMemory = DeviceInfo.getTotalMemorySync();
224
231
  const usedMemory = DeviceInfo.getUsedMemorySync();
225
- const reactNativeVersion = getReactNativeVersion();
226
232
 
227
233
  // Try to get async device info (battery, carrier)
228
234
  let batteryLevel: string | undefined;
@@ -265,7 +271,7 @@ async function collectMobileMeta(): Promise<PreloadedMobileMeta> {
265
271
  }
266
272
 
267
273
  const attributes: SessionAttributes = {
268
- react_native_version: reactNativeVersion,
274
+ ...minimalSessionDeviceAttributes(),
269
275
  device_os: systemName,
270
276
  device_os_version: systemVersion,
271
277
  device_os_detail: deviceOsDetail,
@@ -0,0 +1,410 @@
1
+ import { EVENT_SESSION_START, EVENT_VIEW_CHANGED, initializeFaro, TransportItemType } from '@grafana/faro-core';
2
+ import type { TransportItem } from '@grafana/faro-core';
3
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
4
+
5
+ import { ViewInstrumentation } from '../view';
6
+
7
+ import { SessionInstrumentation } from './index';
8
+ import { MmkvPersistentSessionsManager } from './sessionManager/MmkvPersistentSessionsManager';
9
+ import { VolatileSessionsManager } from './sessionManager/VolatileSessionManager';
10
+
11
+ async function initializeWithoutInstrumentations() {
12
+ return initializeFaro(
13
+ mockConfig({
14
+ instrumentations: [],
15
+ transports: [new MockTransport()],
16
+ url: 'http://localhost:12345/collect',
17
+ sessionTracking: { enabled: true, persistent: false },
18
+ })
19
+ );
20
+ }
21
+
22
+ function sessionStartCount(transport: MockTransport): number {
23
+ return transport.items.filter(
24
+ (item) => item.type === TransportItemType.EVENT && item.payload.name === EVENT_SESSION_START
25
+ ).length;
26
+ }
27
+
28
+ function sessionItem(isSampled: boolean): TransportItem {
29
+ return {
30
+ type: TransportItemType.EVENT,
31
+ payload: { name: 'test' },
32
+ meta: {
33
+ session: {
34
+ id: 'active-session',
35
+ attributes: { isSampled: isSampled.toString() },
36
+ },
37
+ },
38
+ };
39
+ }
40
+
41
+ function expectSampledItem(result: TransportItem | null): TransportItem {
42
+ expect(result).not.toBeNull();
43
+ if (result == null) {
44
+ throw new Error('Expected a sampled item to be retained.');
45
+ }
46
+
47
+ expect(result.payload).toEqual({ name: 'test' });
48
+ expect(result.meta.session?.attributes?.['isSampled']).toBeUndefined();
49
+ return result;
50
+ }
51
+
52
+ describe('session cleanup', () => {
53
+ beforeEach(() => {
54
+ jest.restoreAllMocks();
55
+ jest.clearAllMocks();
56
+ VolatileSessionsManager.removeUserSession();
57
+ });
58
+
59
+ afterEach(() => {
60
+ VolatileSessionsManager.removeUserSession();
61
+ });
62
+
63
+ it('removes the volatile manager metas listener with the same handler', async () => {
64
+ const faro = await initializeWithoutInstrumentations();
65
+ const addListenerSpy = jest.spyOn(faro.metas, 'addListener');
66
+ const removeListenerSpy = jest.spyOn(faro.metas, 'removeListener');
67
+ const storeSessionSpy = jest.spyOn(VolatileSessionsManager, 'storeUserSession');
68
+ const manager = new VolatileSessionsManager();
69
+ const handler = addListenerSpy.mock.calls[0]?.[0];
70
+
71
+ expect(handler).toBeDefined();
72
+ faro.api.setSession({ id: 'before-cleanup' });
73
+ expect(storeSessionSpy).toHaveBeenCalled();
74
+ storeSessionSpy.mockClear();
75
+
76
+ manager.unpatch();
77
+ manager.unpatch();
78
+
79
+ expect(removeListenerSpy).toHaveBeenCalledTimes(1);
80
+ expect(removeListenerSpy).toHaveBeenCalledWith(handler);
81
+
82
+ faro.api.setSession({ id: 'after-cleanup' });
83
+ expect(storeSessionSpy).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('removes the persistent manager metas listener with the same handler', async () => {
87
+ const faro = await initializeWithoutInstrumentations();
88
+ const addListenerSpy = jest.spyOn(faro.metas, 'addListener');
89
+ const removeListenerSpy = jest.spyOn(faro.metas, 'removeListener');
90
+ jest.spyOn(MmkvPersistentSessionsManager, 'fetchUserSession').mockReturnValue(null);
91
+ const storeSessionSpy = jest.spyOn(MmkvPersistentSessionsManager, 'storeUserSession').mockImplementation();
92
+ const manager = new MmkvPersistentSessionsManager();
93
+ const handler = addListenerSpy.mock.calls[0]?.[0];
94
+
95
+ expect(handler).toBeDefined();
96
+ faro.api.setSession({ id: 'before-cleanup' });
97
+ expect(storeSessionSpy).toHaveBeenCalled();
98
+ storeSessionSpy.mockClear();
99
+
100
+ manager.unpatch();
101
+ manager.unpatch();
102
+
103
+ expect(removeListenerSpy).toHaveBeenCalledTimes(1);
104
+ expect(removeListenerSpy).toHaveBeenCalledWith(handler);
105
+
106
+ faro.api.setSession({ id: 'after-cleanup' });
107
+ expect(storeSessionSpy).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('cleans up through destroy while a retained hook still enforces sampling', async () => {
111
+ const transport = new MockTransport();
112
+ const faro = initializeFaro(
113
+ mockConfig({
114
+ instrumentations: [],
115
+ transports: [transport],
116
+ url: 'http://localhost:12345/collect',
117
+ sessionTracking: { enabled: true, persistent: false },
118
+ })
119
+ );
120
+ const addListenerSpy = jest.spyOn(faro.metas, 'addListener');
121
+ const removeListenerSpy = jest.spyOn(faro.metas, 'removeListener');
122
+ const removeBeforeSendHooksSpy = jest.spyOn(faro.transports, 'removeBeforeSendHooks');
123
+ const instrumentation = new SessionInstrumentation();
124
+
125
+ faro.instrumentations.add(instrumentation);
126
+
127
+ const registeredListeners = addListenerSpy.mock.calls.map(([listener]) => listener);
128
+ expect(registeredListeners).toHaveLength(2);
129
+
130
+ const beforeSendHooks = faro.transports.getBeforeSendHooks();
131
+ expect(beforeSendHooks).toHaveLength(2);
132
+ const [fallbackHook, beforeSendHook] = beforeSendHooks;
133
+ if (fallbackHook == null || beforeSendHook == null) {
134
+ throw new Error('Expected the sampling fallback and session before-send hooks to be registered.');
135
+ }
136
+ expectSampledItem(beforeSendHook(sessionItem(true)));
137
+
138
+ const storedSession = VolatileSessionsManager.fetchUserSession();
139
+ if (storedSession == null) {
140
+ throw new Error('Expected an active volatile session before cleanup.');
141
+ }
142
+ const unsampledSession = {
143
+ ...storedSession,
144
+ isSampled: false,
145
+ sessionMeta: {
146
+ ...storedSession.sessionMeta,
147
+ id: storedSession.sessionId,
148
+ attributes: {
149
+ ...storedSession.sessionMeta?.attributes,
150
+ isSampled: 'false',
151
+ },
152
+ },
153
+ };
154
+ VolatileSessionsManager.storeUserSession(unsampledSession);
155
+ faro.api.setSession(unsampledSession.sessionMeta);
156
+ expect(faro.api.getSession()?.attributes?.['isSampled']).toBe('false');
157
+ const sessionStartsBeforeDestroy = sessionStartCount(transport);
158
+ instrumentation.destroy();
159
+
160
+ expect(removeListenerSpy).toHaveBeenCalledTimes(2);
161
+ expect(new Set(removeListenerSpy.mock.calls.map(([listener]) => listener))).toStrictEqual(
162
+ new Set(registeredListeners)
163
+ );
164
+ expect(removeBeforeSendHooksSpy).toHaveBeenCalledWith(beforeSendHook);
165
+ // A detached session hook must not rotate sessions, but a captured queued
166
+ // item still carries the sampling decision that the hook must enforce.
167
+ expect(beforeSendHook(sessionItem(false))).toBeNull();
168
+ expectSampledItem(beforeSendHook(sessionItem(true)));
169
+ expect(fallbackHook(sessionItem(false))).toBeNull();
170
+ expectSampledItem(fallbackHook(sessionItem(true)));
171
+
172
+ expect(faro.api.getSession()?.attributes?.['isSampled']).toBeUndefined();
173
+ const itemsAfterDestroy = transport.items.length;
174
+ faro.transports.execute(sessionItem(false));
175
+ expect(transport.items).toHaveLength(itemsAfterDestroy);
176
+ faro.transports.execute(sessionItem(true));
177
+ expect(transport.items).toHaveLength(itemsAfterDestroy + 1);
178
+ expect(transport.items.at(-1)?.meta.session?.attributes?.['isSampled']).toBeUndefined();
179
+
180
+ faro.api.pushEvent('after-destroy');
181
+ expect(transport.items.at(-1)?.payload).toEqual(expect.objectContaining({ name: 'after-destroy' }));
182
+ expect(transport.items.at(-1)?.meta.session?.attributes?.['isSampled']).toBeUndefined();
183
+
184
+ faro.api.setSession({ id: 'after-cleanup' });
185
+ expect(sessionStartCount(transport)).toBe(sessionStartsBeforeDestroy);
186
+
187
+ instrumentation.destroy();
188
+ expect(removeListenerSpy).toHaveBeenCalledTimes(2);
189
+ expect(removeBeforeSendHooksSpy).toHaveBeenCalledTimes(1);
190
+ });
191
+
192
+ it.each([
193
+ ['sampled', 1, 1],
194
+ ['unsampled', 0, 0],
195
+ ] as const)('keeps teardown listener telemetry attributed for a %s session', async (_name, rate, expectedCount) => {
196
+ const transport = new MockTransport();
197
+ const faro = initializeFaro(
198
+ mockConfig({
199
+ instrumentations: [],
200
+ transports: [transport],
201
+ url: 'http://localhost:12345/collect',
202
+ view: { name: 'configured-view' },
203
+ sessionTracking: {
204
+ enabled: true,
205
+ persistent: false,
206
+ sampling: { resolve: () => rate },
207
+ },
208
+ })
209
+ );
210
+ const instrumentation = new SessionInstrumentation();
211
+ const checkSessionSpy = jest.spyOn(VolatileSessionsManager.prototype, 'checkSession');
212
+ faro.instrumentations.add(instrumentation);
213
+ faro.instrumentations.add(new ViewInstrumentation());
214
+ const storedSession = VolatileSessionsManager.fetchUserSession();
215
+ if (storedSession == null) {
216
+ throw new Error('Expected an active volatile session before cleanup.');
217
+ }
218
+ expect(storedSession.isSampled).toBe(rate === 1);
219
+ checkSessionSpy.mockClear();
220
+ const itemsBeforeDestroy = transport.items.length;
221
+
222
+ instrumentation.destroy();
223
+
224
+ expect(checkSessionSpy).not.toHaveBeenCalled();
225
+ const teardownItems = transport.items.slice(itemsBeforeDestroy);
226
+ const viewChangedItems = teardownItems.filter(
227
+ (item) => item.type === TransportItemType.EVENT && item.payload.name === EVENT_VIEW_CHANGED
228
+ );
229
+ expect(viewChangedItems).toHaveLength(expectedCount);
230
+ if (expectedCount === 1) {
231
+ expect(viewChangedItems[0]?.meta.session?.id).toBe(storedSession.sessionId);
232
+ expect(viewChangedItems[0]?.meta.session?.attributes?.['isSampled']).toBeUndefined();
233
+ }
234
+ });
235
+
236
+ it('finishes cleanup when a metas listener throws during the sampling scrub', async () => {
237
+ const faro = await initializeWithoutInstrumentations();
238
+ const instrumentation = new SessionInstrumentation();
239
+ const removeBeforeSendHooksSpy = jest.spyOn(faro.transports, 'removeBeforeSendHooks');
240
+ const checkSessionSpy = jest.spyOn(VolatileSessionsManager.prototype, 'checkSession');
241
+ faro.instrumentations.add(instrumentation);
242
+ const beforeSendHook = faro.transports.getBeforeSendHooks().at(-1);
243
+ if (beforeSendHook == null) {
244
+ throw new Error('Expected a session before-send hook before cleanup.');
245
+ }
246
+ const throwingListener = jest.fn((meta) => {
247
+ if (meta.session == null) {
248
+ throw new Error('metas listener failed');
249
+ }
250
+ });
251
+ faro.metas.addListener(throwingListener);
252
+
253
+ expect(() => instrumentation.destroy()).toThrow('metas listener failed');
254
+ expect(removeBeforeSendHooksSpy).toHaveBeenCalledTimes(1);
255
+ expect(removeBeforeSendHooksSpy).toHaveBeenCalledWith(beforeSendHook);
256
+
257
+ faro.metas.removeListener(throwingListener);
258
+ checkSessionSpy.mockClear();
259
+ faro.api.pushEvent('after-failed-cleanup');
260
+ expect(checkSessionSpy).not.toHaveBeenCalled();
261
+
262
+ expect(() => instrumentation.destroy()).not.toThrow();
263
+ expect(removeBeforeSendHooksSpy).toHaveBeenCalledTimes(1);
264
+ });
265
+
266
+ it('binds a manager listener to the supplied metas instance', async () => {
267
+ const registeredFaro = await initializeWithoutInstrumentations();
268
+ const addListenerSpy = jest.spyOn(registeredFaro.metas, 'addListener');
269
+ const removeRegisteredListenerSpy = jest.spyOn(registeredFaro.metas, 'removeListener');
270
+ const replacementFaro = await initializeWithoutInstrumentations();
271
+ const addReplacementListenerSpy = jest.spyOn(replacementFaro.metas, 'addListener');
272
+ const removeReplacementListenerSpy = jest.spyOn(replacementFaro.metas, 'removeListener');
273
+ const manager = new VolatileSessionsManager(registeredFaro.metas);
274
+ const handler = addListenerSpy.mock.calls[0]?.[0];
275
+
276
+ manager.unpatch();
277
+
278
+ expect(addListenerSpy).toHaveBeenCalledWith(handler);
279
+ expect(addReplacementListenerSpy).not.toHaveBeenCalled();
280
+ expect(removeRegisteredListenerSpy).toHaveBeenCalledWith(handler);
281
+ expect(removeReplacementListenerSpy).not.toHaveBeenCalled();
282
+ });
283
+
284
+ it('binds a persistent manager listener to the supplied metas instance', async () => {
285
+ const registeredFaro = await initializeWithoutInstrumentations();
286
+ const addListenerSpy = jest.spyOn(registeredFaro.metas, 'addListener');
287
+ const removeRegisteredListenerSpy = jest.spyOn(registeredFaro.metas, 'removeListener');
288
+ const replacementFaro = await initializeWithoutInstrumentations();
289
+ const addReplacementListenerSpy = jest.spyOn(replacementFaro.metas, 'addListener');
290
+ const removeReplacementListenerSpy = jest.spyOn(replacementFaro.metas, 'removeListener');
291
+ jest.spyOn(MmkvPersistentSessionsManager, 'fetchUserSession').mockReturnValue(null);
292
+ jest.spyOn(MmkvPersistentSessionsManager, 'storeUserSession').mockImplementation();
293
+ const manager = new MmkvPersistentSessionsManager(registeredFaro.metas);
294
+ const handler = addListenerSpy.mock.calls[0]?.[0];
295
+
296
+ manager.unpatch();
297
+
298
+ expect(addListenerSpy).toHaveBeenCalledWith(handler);
299
+ expect(addReplacementListenerSpy).not.toHaveBeenCalled();
300
+ expect(removeRegisteredListenerSpy).toHaveBeenCalledWith(handler);
301
+ expect(removeReplacementListenerSpy).not.toHaveBeenCalled();
302
+ });
303
+
304
+ it('moves every registration when an instrumentation instance is reused', async () => {
305
+ const instrumentation = new SessionInstrumentation();
306
+ const firstTransport = new MockTransport();
307
+ const firstFaro = initializeFaro(
308
+ mockConfig({
309
+ instrumentations: [],
310
+ transports: [firstTransport],
311
+ url: 'http://localhost:12345/collect',
312
+ sessionTracking: { enabled: true, persistent: false },
313
+ })
314
+ );
315
+ const firstRemoveListenerSpy = jest.spyOn(firstFaro.metas, 'removeListener');
316
+ const firstRemoveBeforeSendHooksSpy = jest.spyOn(firstFaro.transports, 'removeBeforeSendHooks');
317
+ const firstSetSessionSpy = jest.spyOn(firstFaro.api, 'setSession');
318
+ firstFaro.instrumentations.add(instrumentation);
319
+ const firstHook = firstFaro.transports.getBeforeSendHooks().at(-1);
320
+ if (firstHook == null) {
321
+ throw new Error('Expected the first Faro instance to retain its session hook.');
322
+ }
323
+
324
+ const secondTransport = new MockTransport();
325
+ const secondFaro = initializeFaro(
326
+ mockConfig({
327
+ instrumentations: [],
328
+ transports: [secondTransport],
329
+ url: 'http://localhost:12345/collect',
330
+ sessionTracking: { enabled: true, persistent: false },
331
+ })
332
+ );
333
+ const secondAddListenerSpy = jest.spyOn(secondFaro.metas, 'addListener');
334
+ const secondRemoveListenerSpy = jest.spyOn(secondFaro.metas, 'removeListener');
335
+ secondFaro.instrumentations.add(instrumentation);
336
+
337
+ expect(firstRemoveListenerSpy).toHaveBeenCalledTimes(2);
338
+ expect(firstRemoveBeforeSendHooksSpy).toHaveBeenCalledWith(firstHook);
339
+ expect(firstSetSessionSpy).toHaveBeenCalledWith(
340
+ expect.objectContaining({
341
+ attributes: expect.not.objectContaining({ isSampled: expect.anything() }),
342
+ })
343
+ );
344
+ expect(secondAddListenerSpy).toHaveBeenCalledTimes(2);
345
+ const firstHookResult = expectSampledItem(firstHook(sessionItem(true)));
346
+ expect(firstHookResult.meta.session?.id).toBe('active-session');
347
+
348
+ const secondSessionStarts = sessionStartCount(secondTransport);
349
+ firstFaro.api.setSession({ id: 'stale-faro-session' });
350
+ expect(sessionStartCount(secondTransport)).toBe(secondSessionStarts);
351
+
352
+ secondFaro.api.setSession({ id: 'active-faro-session' });
353
+ expect(sessionStartCount(secondTransport)).toBe(secondSessionStarts + 1);
354
+
355
+ instrumentation.destroy();
356
+ expect(secondRemoveListenerSpy).toHaveBeenCalledTimes(2);
357
+
358
+ secondFaro.api.setSession({ id: 'after-destroy' });
359
+ expect(sessionStartCount(secondTransport)).toBe(secondSessionStarts + 1);
360
+ });
361
+
362
+ it('lets a replacement hook reattribute items retained by an older Faro Core', async () => {
363
+ const faro = await initializeWithoutInstrumentations();
364
+ const firstInstrumentation = new SessionInstrumentation();
365
+ faro.instrumentations.add(firstInstrumentation);
366
+ const firstHook = faro.transports.getBeforeSendHooks().at(-1);
367
+ if (firstHook == null) {
368
+ throw new Error('Expected the first instrumentation to register a session hook.');
369
+ }
370
+
371
+ faro.instrumentations.remove(firstInstrumentation);
372
+ expect(faro.instrumentations.instrumentations).not.toContain(firstInstrumentation);
373
+ expect(faro.instrumentations.instrumentations).toHaveLength(0);
374
+
375
+ const replacementInstrumentation = new SessionInstrumentation();
376
+ faro.instrumentations.add(replacementInstrumentation);
377
+ expect(faro.instrumentations.instrumentations).toContain(replacementInstrumentation);
378
+
379
+ const activeSession = VolatileSessionsManager.fetchUserSession();
380
+ if (activeSession == null) {
381
+ throw new Error('Expected the replacement instrumentation to own a session.');
382
+ }
383
+
384
+ const installedHooks = faro.transports.getBeforeSendHooks();
385
+ const hooks = installedHooks.includes(firstHook) ? installedHooks : [firstHook, ...installedHooks];
386
+ const result = hooks.reduce<TransportItem | null>(
387
+ (item, hook) => (item == null ? null : hook(item)),
388
+ sessionItem(false)
389
+ );
390
+ const sampledResult = expectSampledItem(result);
391
+
392
+ expect(sampledResult.meta.session?.id).toBe(activeSession.sessionId);
393
+ replacementInstrumentation.destroy();
394
+ });
395
+
396
+ it('registers its manager listener on the instrumentation metas instance', async () => {
397
+ const ownerFaro = await initializeWithoutInstrumentations();
398
+ const ownerAddListenerSpy = jest.spyOn(ownerFaro.metas, 'addListener');
399
+ const otherFaro = await initializeWithoutInstrumentations();
400
+ const otherAddListenerSpy = jest.spyOn(otherFaro.metas, 'addListener');
401
+ const instrumentation = new SessionInstrumentation();
402
+
403
+ ownerFaro.instrumentations.add(instrumentation);
404
+
405
+ expect(ownerAddListenerSpy).toHaveBeenCalledTimes(2);
406
+ expect(otherAddListenerSpy).not.toHaveBeenCalled();
407
+
408
+ instrumentation.destroy();
409
+ });
410
+ });