@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,155 @@
1
+ import { NativeModules, Platform } from 'react-native';
2
+
3
+ import { initializeFaro, type MeasurementEvent, type TransportItem } from '@grafana/faro-core';
4
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
5
+
6
+ import { FrameMonitoringInstrumentation } from './instrumentation';
7
+
8
+ jest.mock('react-native', () => ({
9
+ Platform: { OS: 'android' },
10
+ NativeModules: {
11
+ FaroReactNativeModule: {
12
+ startFrameMonitoring: jest.fn(),
13
+ getFrameMetrics: jest.fn(),
14
+ getRefreshRate: jest.fn().mockResolvedValue(null),
15
+ stopFrameMonitoring: jest.fn(),
16
+ },
17
+ },
18
+ NativeEventEmitter: jest.fn().mockImplementation(() => ({
19
+ addListener: jest.fn(() => ({ remove: jest.fn() })),
20
+ })),
21
+ }));
22
+
23
+ const POLL_INTERVAL_MS = 1000;
24
+
25
+ function getNativeModule() {
26
+ return NativeModules.FaroReactNativeModule as {
27
+ startFrameMonitoring: jest.Mock;
28
+ getFrameMetrics: jest.Mock;
29
+ getRefreshRate: jest.Mock;
30
+ stopFrameMonitoring: jest.Mock;
31
+ };
32
+ }
33
+
34
+ function getFrozenFrameMeasurements(transport: MockTransport): MeasurementEvent[] {
35
+ return transport.items
36
+ .map((item) => item as TransportItem<MeasurementEvent>)
37
+ .filter((item) => item.type === 'measurement' && item.payload.type === 'app_frozen_frame')
38
+ .map((item) => item.payload);
39
+ }
40
+
41
+ describe('FrameMonitoringInstrumentation', () => {
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+ jest.useFakeTimers();
45
+ getNativeModule().getFrameMetrics.mockResolvedValue(null);
46
+ getNativeModule().getRefreshRate.mockResolvedValue(null);
47
+ });
48
+
49
+ afterEach(() => {
50
+ jest.useRealTimers();
51
+ });
52
+
53
+ it('sends app_frozen_frame once per poll when frozen frames are reported', async () => {
54
+ const transport = new MockTransport();
55
+ getNativeModule().getFrameMetrics.mockResolvedValue({
56
+ refreshRate: 60,
57
+ slowFrames: 0,
58
+ frozenFrames: 2,
59
+ frozenDurationMs: 1550,
60
+ });
61
+
62
+ initializeFaro(
63
+ mockConfig({
64
+ refreshRateVitals: true,
65
+ transports: [transport],
66
+ instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
67
+ })
68
+ );
69
+
70
+ await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
71
+
72
+ const measurements = getFrozenFrameMeasurements(transport);
73
+ expect(measurements).toHaveLength(1);
74
+ expect(measurements[0].values).toEqual({
75
+ frozen_frames: 2,
76
+ frozen_duration: 1550,
77
+ });
78
+ });
79
+
80
+ it('skips app_frozen_frame when frozen frame count is zero', async () => {
81
+ const transport = new MockTransport();
82
+ getNativeModule().getFrameMetrics.mockResolvedValue({
83
+ refreshRate: 60,
84
+ slowFrames: 0,
85
+ frozenFrames: 0,
86
+ frozenDurationMs: 0,
87
+ });
88
+
89
+ initializeFaro(
90
+ mockConfig({
91
+ refreshRateVitals: true,
92
+ transports: [transport],
93
+ instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
94
+ })
95
+ );
96
+
97
+ await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
98
+
99
+ expect(getFrozenFrameMeasurements(transport)).toHaveLength(0);
100
+ });
101
+
102
+ it('does not send app_frozen_frame when duration is zero even if count is positive', async () => {
103
+ const transport = new MockTransport();
104
+ getNativeModule().getFrameMetrics.mockResolvedValue({
105
+ refreshRate: 60,
106
+ slowFrames: 0,
107
+ frozenFrames: 1,
108
+ frozenDurationMs: 0,
109
+ });
110
+
111
+ initializeFaro(
112
+ mockConfig({
113
+ refreshRateVitals: true,
114
+ transports: [transport],
115
+ instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
116
+ })
117
+ );
118
+
119
+ await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
120
+
121
+ expect(getFrozenFrameMeasurements(transport)).toHaveLength(0);
122
+ });
123
+
124
+ it('reports frozen frames through polling only on Android (no duplicate event path)', async () => {
125
+ const transport = new MockTransport();
126
+ getNativeModule()
127
+ .getFrameMetrics.mockResolvedValueOnce({
128
+ refreshRate: 60,
129
+ slowFrames: 0,
130
+ frozenFrames: 1,
131
+ frozenDurationMs: 850,
132
+ })
133
+ .mockResolvedValueOnce({
134
+ refreshRate: 60,
135
+ slowFrames: 0,
136
+ frozenFrames: 0,
137
+ frozenDurationMs: 0,
138
+ });
139
+
140
+ initializeFaro(
141
+ mockConfig({
142
+ refreshRateVitals: true,
143
+ transports: [transport],
144
+ instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
145
+ })
146
+ );
147
+
148
+ await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
149
+ await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
150
+
151
+ expect(getFrozenFrameMeasurements(transport)).toHaveLength(1);
152
+ expect(Platform.OS).toBe('android');
153
+ expect(getNativeModule().startFrameMonitoring).toHaveBeenCalledTimes(1);
154
+ });
155
+ });
@@ -7,11 +7,11 @@ import type { ReactNativeConfig } from '../../config/types';
7
7
  import type { FrameMetrics, FrameMonitoringOptions } from './types';
8
8
 
9
9
  /**
10
- * Default configuration values matching Flutter SDK's hardcoded values.
11
- * These are React Native-specific advanced options for customization.
10
+ * Default configuration values. Frozen frame threshold aligns with Android Vitals
11
+ * and OpenTelemetry Android slow rendering instrumentation (700ms).
12
12
  */
13
13
  const DEFAULT_TARGET_FPS = 60;
14
- const DEFAULT_FROZEN_FRAME_THRESHOLD_MS = 100;
14
+ const DEFAULT_FROZEN_FRAME_THRESHOLD_MS = 700;
15
15
  const DEFAULT_REFRESH_RATE_POLLING_INTERVAL = 30000;
16
16
  const DEFAULT_NORMALIZED_REFRESH_RATE = 60;
17
17
 
@@ -29,7 +29,7 @@ const DEFAULT_NORMALIZED_REFRESH_RATE = 60;
29
29
  * - Reports meaningful performance degradation that affects user experience
30
30
  *
31
31
  * ## Frozen Frame Detection
32
- * Frozen frames are individual frames exceeding `frozenFrameThresholdMs` (default 100ms).
32
+ * Frozen frames are individual frames exceeding `frozenFrameThresholdMs` (default 700ms).
33
33
  * These represent severe jank where the app appears unresponsive.
34
34
  *
35
35
  * Sends measurements via Faro API:
@@ -46,7 +46,7 @@ const DEFAULT_NORMALIZED_REFRESH_RATE = 60;
46
46
  * instrumentations: [
47
47
  * new FrameMonitoringInstrumentation({
48
48
  * targetFps: 60,
49
- * frozenFrameThresholdMs: 100,
49
+ * frozenFrameThresholdMs: 700,
50
50
  * refreshRatePollingInterval: 30000,
51
51
  * }),
52
52
  * ],
@@ -94,7 +94,7 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
94
94
  // Start native frame monitoring with configuration
95
95
  this.startNativeMonitoring(nativeModule);
96
96
 
97
- // Set up event listeners for real-time frame events (Android pattern)
97
+ // Set up Android refresh rate events (throttled on native side); frozen/slow frames use polling
98
98
  this.setupEventListeners(nativeModule);
99
99
 
100
100
  // Set up polling for periodic metrics collection (iOS pattern)
@@ -127,35 +127,24 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
127
127
  }
128
128
 
129
129
  private setupEventListeners(nativeModule: typeof NativeModules.FaroReactNativeModule): void {
130
- // Android uses event-based approach for frozen frames only
131
- // Slow frames are retrieved via polling (like iOS) to avoid flooding
132
- if (Platform.OS === 'android') {
130
+ // Android: refresh rate only. Slow and frozen frames are polled (same as iOS) to avoid duplicate reports.
131
+ if (Platform.OS === 'android' && this.refreshRateVitalsEnabled) {
133
132
  try {
134
133
  this.eventEmitter = new NativeEventEmitter(nativeModule);
135
134
 
136
- // Listen for frozen frame events
137
- const frozenFrameSubscription = this.eventEmitter.addListener(
138
- 'onFrozenFrame',
139
- (data: number | { count: number; durationMs: number }) => {
140
- // Support both formats: number (legacy) or object (with duration)
141
- if (typeof data === 'number') {
142
- this.handleFrozenFrame(data, 0);
143
- } else {
144
- this.handleFrozenFrame(data.count, data.durationMs || 0);
145
- }
135
+ const refreshRateSubscription = this.eventEmitter.addListener('onRefreshRate', (...args: readonly Object[]) => {
136
+ const [refreshRate] = args;
137
+
138
+ if (typeof refreshRate !== 'number') {
139
+ this.logWarn('Ignoring onRefreshRate event without a numeric payload');
140
+ return;
146
141
  }
147
- );
148
- this.eventSubscriptions.push(frozenFrameSubscription);
149
142
 
150
- // Listen for refresh rate events (only when refreshRateVitals is enabled in config)
151
- if (this.refreshRateVitalsEnabled) {
152
- const refreshRateSubscription = this.eventEmitter.addListener('onRefreshRate', (refreshRate: number) => {
153
- this.handleRefreshRate(refreshRate);
154
- });
155
- this.eventSubscriptions.push(refreshRateSubscription);
156
- }
143
+ this.handleRefreshRate(refreshRate);
144
+ });
145
+ this.eventSubscriptions.push(refreshRateSubscription);
157
146
 
158
- this.logDebug('Set up Android frame event listeners');
147
+ this.logDebug('Set up Android refresh rate event listener');
159
148
  } catch (error) {
160
149
  this.logError('Failed to set up frame event listeners', error);
161
150
  }
@@ -163,8 +152,7 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
163
152
  }
164
153
 
165
154
  private startPolling(): void {
166
- // Both iOS and Android use polling approach for slow frames
167
- // This provides consistent behavior and avoids flooding with events
155
+ // Both iOS and Android use polling for slow and frozen frames (avoids duplicate reports on Android).
168
156
  this.pollingIntervalId = setInterval(() => {
169
157
  this.pollFrameMetrics();
170
158
  }, this.options.refreshRatePollingInterval);
@@ -230,14 +218,7 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
230
218
  }
231
219
 
232
220
  private handleFrozenFrame(count: number, durationMs: number): void {
233
- // Only send frozen frame events if duration is greater than 0
234
- // This filters out any erroneous 0ms frozen frames
235
221
  if (durationMs > 0) {
236
- // 🔍 TEMP DEBUG LOG - Remove after analysis
237
- console.log(
238
- `[Faro DEBUG ${Platform.OS.toUpperCase()}] 🧊 SENDING frozen frame: count=${count}, duration=${durationMs}ms`
239
- );
240
-
241
222
  this.api.pushMeasurement(
242
223
  {
243
224
  type: 'app_frozen_frame',
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * Configuration options for Frame Monitoring instrumentation.
3
3
  *
4
- * These are React Native-specific advanced options. The Flutter SDK uses fixed values
5
- * for these parameters (not configurable). Defaults here match Flutter's hardcoded values
6
- * for cross-platform consistency.
4
+ * Frozen frame threshold (700ms).
7
5
  *
8
6
  * Note: To enable frame monitoring, set `refreshRateVitals: true` in ReactNativeConfig.
9
7
  */
@@ -11,14 +9,14 @@ export interface FrameMonitoringOptions {
11
9
  /**
12
10
  * Target frames per second for slow frame detection.
13
11
  * Frames rendering below this threshold are considered "slow".
14
- * Default: 60 (Flutter SDK hardcoded value)
12
+ * Default: 60
15
13
  */
16
14
  targetFps?: number;
17
15
 
18
16
  /**
19
17
  * Threshold in milliseconds for detecting frozen frames.
20
18
  * Frames taking longer than this are considered "frozen".
21
- * Default: 100ms (Flutter SDK uses 100_000_000 nanoseconds)
19
+ * Default: 700ms (Android Vitals and OpenTelemetry Android slow rendering instrumentation)
22
20
  */
23
21
  frozenFrameThresholdMs?: number;
24
22
 
@@ -31,7 +29,7 @@ export interface FrameMonitoringOptions {
31
29
  /**
32
30
  * Normalized refresh rate for ProMotion displays (120Hz, etc.).
33
31
  * Used to normalize frame rates for high-refresh-rate displays to a standard baseline.
34
- * Default: 60 (Flutter SDK's backendSupportedFrameRate)
32
+ * Default: 60
35
33
  */
36
34
  normalizedRefreshRate?: number;
37
35
  }
@@ -13,8 +13,6 @@ const { FaroReactNativeModule } = NativeModules;
13
13
  * - iOS: task_info() for memory, host_statistics() for CPU
14
14
  * - Android: /proc/[pid]/status for memory, /proc/[pid]/stat for CPU
15
15
  *
16
- * Implementation ported from Faro Flutter SDK with feature parity.
17
- *
18
16
  * **Key Features**:
19
17
  * - ✅ NO manual setup required - OS tracks metrics automatically!
20
18
  * - ✅ Periodic collection (default: every 30 seconds)
@@ -143,16 +141,14 @@ export class PerformanceInstrumentation extends BaseInstrumentation {
143
141
  const cpuUsage = FaroReactNativeModule.getCpuUsage();
144
142
  this.logDebug(`CPU: raw value = ${cpuUsage}`);
145
143
 
146
- // Validate CPU usage (Flutter SDK filters 0-100 range, but allows >100)
147
- // Skip null, negative, or exactly 0 (baseline reading)
144
+ // Validate CPU usage. Skip null, negative, or exactly 0 (baseline reading).
148
145
  if (cpuUsage == null || cpuUsage <= 0) {
149
146
  this.logDebug('CPU: skipping - value is null or <= 0');
150
147
  return;
151
148
  }
152
149
 
153
150
  this.logDebug(`CPU: pushing measurement with cpu_usage = ${cpuUsage}`);
154
- // Flutter SDK also filters values >= 100, but we allow them as they can be valid
155
- // in multi-core scenarios where one core is maxed out
151
+ // Values >= 100 can be valid in multi-core scenarios where one core is maxed out.
156
152
  this.api.pushMeasurement({
157
153
  type: 'app_cpu_usage',
158
154
  values: { cpu_usage: cpuUsage },
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import { act, create, type ReactTestRenderer } from 'react-test-renderer';
4
+
5
+ import * as directSessionActivity from './directSessionActivity';
6
+ import { FaroSessionActivityBoundary } from './FaroSessionActivityBoundary';
7
+
8
+ describe('FaroSessionActivityBoundary', () => {
9
+ let renderer: ReactTestRenderer | undefined;
10
+
11
+ afterEach(() => {
12
+ if (renderer != null) {
13
+ const mountedRenderer = renderer;
14
+ act(() => mountedRenderer.unmount());
15
+ renderer = undefined;
16
+ }
17
+ jest.restoreAllMocks();
18
+ });
19
+
20
+ it('renders a boundary that records a captured touch without claiming the responder', async () => {
21
+ const notifySpy = jest.spyOn(directSessionActivity, 'notifySessionActivity').mockImplementation();
22
+ const child = React.createElement(Text, null, 'Press me');
23
+ const event = { nativeEvent: {} } as never;
24
+ await act(async () => {
25
+ renderer = create(React.createElement(FaroSessionActivityBoundary, null, child));
26
+ });
27
+ if (renderer == null) {
28
+ throw new Error('Expected the activity boundary to render.');
29
+ }
30
+
31
+ const boundary = renderer.root.findByType(View);
32
+ const claimsResponder = boundary.props.onStartShouldSetResponderCapture(event);
33
+
34
+ expect(notifySpy).toHaveBeenCalledTimes(1);
35
+ expect(claimsResponder).toBe(false);
36
+ expect(renderer.root.findByType(Text).props.children).toBe('Press me');
37
+ });
38
+
39
+ it('preserves a caller responder decision and layout override', async () => {
40
+ const onStartShouldSetResponderCapture = jest.fn(() => true);
41
+ const event = { nativeEvent: {} } as never;
42
+ await act(async () => {
43
+ renderer = create(
44
+ React.createElement(FaroSessionActivityBoundary, {
45
+ onStartShouldSetResponderCapture,
46
+ style: { flex: 0, height: 40 },
47
+ })
48
+ );
49
+ });
50
+ if (renderer == null) {
51
+ throw new Error('Expected the activity boundary to render.');
52
+ }
53
+
54
+ const boundary = renderer.root.findByType(View);
55
+
56
+ expect(boundary.props.onStartShouldSetResponderCapture(event)).toBe(true);
57
+ expect(onStartShouldSetResponderCapture).toHaveBeenCalledWith(event);
58
+ expect(boundary.props.style).toEqual([{ flex: 1 }, { flex: 0, height: 40 }]);
59
+ });
60
+ });
@@ -0,0 +1,41 @@
1
+ import { type PropsWithChildren, useCallback } from 'react';
2
+ import { type GestureResponderEvent, StyleSheet, View, type ViewProps } from 'react-native';
3
+
4
+ import { notifySessionActivity } from './directSessionActivity';
5
+
6
+ export type FaroSessionActivityBoundaryProps = PropsWithChildren<ViewProps>;
7
+
8
+ /**
9
+ * Records touch activity for the React Native subtree without emitting a user
10
+ * action or taking ownership of the responder.
11
+ */
12
+ export function FaroSessionActivityBoundary({
13
+ children,
14
+ onStartShouldSetResponderCapture,
15
+ style,
16
+ ...viewProps
17
+ }: FaroSessionActivityBoundaryProps) {
18
+ const handleStartShouldSetResponderCapture = useCallback(
19
+ (event: GestureResponderEvent): boolean => {
20
+ notifySessionActivity();
21
+ return onStartShouldSetResponderCapture?.(event) ?? false;
22
+ },
23
+ [onStartShouldSetResponderCapture]
24
+ );
25
+
26
+ return (
27
+ <View
28
+ {...viewProps}
29
+ onStartShouldSetResponderCapture={handleStartShouldSetResponderCapture}
30
+ style={[styles.container, style]}
31
+ >
32
+ {children}
33
+ </View>
34
+ );
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ container: {
39
+ flex: 1,
40
+ },
41
+ });