@grafana/faro-react-native 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +176 -30
  2. package/android/build.gradle +17 -12
  3. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
  4. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
  5. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
  9. package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
  10. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
  11. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
  12. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
  13. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
  14. package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
  15. package/dist/cjs/config/getRNInstrumentations.js +6 -8
  16. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  17. package/dist/cjs/config/sampling.js.map +1 -1
  18. package/dist/cjs/config/types.js.map +1 -1
  19. package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
  20. package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
  21. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  22. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  23. package/dist/cjs/index.js +8 -3
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
  26. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  27. package/dist/cjs/instrumentations/appState/index.js +8 -2
  28. package/dist/cjs/instrumentations/appState/index.js.map +1 -1
  29. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
  30. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  31. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
  32. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  33. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  34. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  35. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  36. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/errors/index.js +8 -3
  39. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
  41. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  42. package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
  43. package/dist/cjs/instrumentations/performance/index.js +2 -6
  44. package/dist/cjs/instrumentations/performance/index.js.map +1 -1
  45. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
  46. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  47. package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
  48. package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
  49. package/dist/cjs/instrumentations/session/index.js +214 -45
  50. package/dist/cjs/instrumentations/session/index.js.map +1 -1
  51. package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
  52. package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
  53. package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
  54. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  55. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
  56. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  57. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
  58. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  59. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
  60. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  61. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  62. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  63. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
  64. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  65. package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
  66. package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
  67. package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
  68. package/dist/cjs/instrumentations/startup/index.js +2 -6
  69. package/dist/cjs/instrumentations/startup/index.js.map +1 -1
  70. package/dist/cjs/navigation/utils.js +3 -1
  71. package/dist/cjs/navigation/utils.js.map +1 -1
  72. package/dist/cjs/transports/fetch/index.js.map +1 -1
  73. package/dist/cjs/transports/fetch/transport.js +56 -25
  74. package/dist/cjs/transports/fetch/transport.js.map +1 -1
  75. package/dist/cjs/transports/fetch/types.js.map +1 -1
  76. package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
  77. package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
  78. package/dist/cjs/transports/offline/OfflineCache.js +2 -2
  79. package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
  80. package/dist/cjs/transports/offline/transport.js +3 -4
  81. package/dist/cjs/transports/offline/transport.js.map +1 -1
  82. package/dist/cjs/userPersistence/UserPersistence.js +1 -2
  83. package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
  84. package/dist/esm/config/getRNInstrumentations.js +6 -8
  85. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  86. package/dist/esm/config/sampling.js.map +1 -1
  87. package/dist/esm/config/types.js.map +1 -1
  88. package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
  89. package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
  90. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  91. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  92. package/dist/esm/index.js +4 -2
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
  95. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  96. package/dist/esm/instrumentations/appState/index.js +8 -2
  97. package/dist/esm/instrumentations/appState/index.js.map +1 -1
  98. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
  99. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  100. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
  101. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  102. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  103. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  104. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  105. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  106. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  107. package/dist/esm/instrumentations/errors/index.js +8 -3
  108. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  109. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
  110. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  111. package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
  112. package/dist/esm/instrumentations/performance/index.js +2 -6
  113. package/dist/esm/instrumentations/performance/index.js.map +1 -1
  114. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
  115. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  116. package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
  117. package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
  118. package/dist/esm/instrumentations/session/index.js +207 -46
  119. package/dist/esm/instrumentations/session/index.js.map +1 -1
  120. package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
  121. package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
  122. package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
  123. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  124. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
  125. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  126. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
  127. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  128. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
  129. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  130. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  131. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  132. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
  133. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  134. package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
  135. package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
  136. package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
  137. package/dist/esm/instrumentations/startup/index.js +2 -6
  138. package/dist/esm/instrumentations/startup/index.js.map +1 -1
  139. package/dist/esm/navigation/utils.js +2 -1
  140. package/dist/esm/navigation/utils.js.map +1 -1
  141. package/dist/esm/transports/fetch/index.js.map +1 -1
  142. package/dist/esm/transports/fetch/transport.js +38 -14
  143. package/dist/esm/transports/fetch/transport.js.map +1 -1
  144. package/dist/esm/transports/fetch/types.js.map +1 -1
  145. package/dist/esm/transports/offline/ConnectivityService.js +1 -1
  146. package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
  147. package/dist/esm/transports/offline/OfflineCache.js +2 -2
  148. package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
  149. package/dist/esm/transports/offline/transport.js +3 -4
  150. package/dist/esm/transports/offline/transport.js.map +1 -1
  151. package/dist/esm/userPersistence/UserPersistence.js +1 -2
  152. package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
  153. package/dist/types/config/getRNInstrumentations.d.ts +1 -2
  154. package/dist/types/config/sampling.d.ts +0 -3
  155. package/dist/types/config/types.d.ts +5 -3
  156. package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
  157. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  158. package/dist/types/index.d.ts +4 -1
  159. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  160. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  161. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
  162. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
  163. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  164. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
  165. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  166. package/dist/types/instrumentations/performance/index.d.ts +0 -2
  167. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  168. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  169. package/dist/types/instrumentations/session/index.d.ts +20 -1
  170. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  171. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  172. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  173. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  174. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  175. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  176. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  177. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  178. package/dist/types/instrumentations/startup/index.d.ts +2 -6
  179. package/dist/types/navigation/utils.d.ts +1 -0
  180. package/dist/types/transports/fetch/index.d.ts +1 -1
  181. package/dist/types/transports/fetch/transport.d.ts +8 -1
  182. package/dist/types/transports/fetch/types.d.ts +5 -0
  183. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  184. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  185. package/dist/types/transports/offline/transport.d.ts +3 -4
  186. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  187. package/ios/FaroCrashReporter.swift +214 -21
  188. package/ios/FaroReactNative.swift +24 -0
  189. package/ios/FaroReactNativeModule.h +2 -1
  190. package/ios/FaroReactNativeModule.mm +121 -6
  191. package/ios/RefreshRateVitals.swift +34 -38
  192. package/package.json +7 -5
  193. package/src/config/getRNInstrumentations.ts +6 -8
  194. package/src/config/makeRNConfig.test.ts +6 -0
  195. package/src/config/sampling.ts +0 -3
  196. package/src/config/types.ts +5 -3
  197. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  198. package/src/generated/faroRNPackageMeta.ts +1 -1
  199. package/src/index.ts +8 -2
  200. package/src/instrumentations/anr/instrumentation.ts +8 -1
  201. package/src/instrumentations/appState/index.ts +10 -2
  202. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  203. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  204. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  205. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  206. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  207. package/src/instrumentations/crashReporting/types.ts +7 -0
  208. package/src/instrumentations/errors/index.ts +12 -4
  209. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  210. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  211. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  212. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  213. package/src/instrumentations/performance/index.ts +2 -6
  214. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  215. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  216. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  217. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  218. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  219. package/src/instrumentations/session/index.ts +269 -46
  220. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  221. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  222. package/src/instrumentations/session/sessionActivity.ts +58 -0
  223. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  224. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  225. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  226. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  227. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  228. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  229. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  230. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  231. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  232. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  233. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  234. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  235. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  236. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  237. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  238. package/src/instrumentations/session/sessionProcess.ts +114 -0
  239. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  240. package/src/instrumentations/startup/index.ts +2 -6
  241. package/src/navigation/utils.ts +3 -1
  242. package/src/transports/fetch/index.ts +6 -1
  243. package/src/transports/fetch/transport.test.ts +86 -2
  244. package/src/transports/fetch/transport.ts +38 -16
  245. package/src/transports/fetch/types.ts +7 -0
  246. package/src/transports/offline/ConnectivityService.ts +1 -1
  247. package/src/transports/offline/OfflineCache.ts +2 -2
  248. package/src/transports/offline/transport.ts +3 -4
  249. package/src/userPersistence/UserPersistence.ts +1 -2
  250. package/dist/cjs/utils/throttle.js +0 -42
  251. package/dist/cjs/utils/throttle.js.map +0 -1
  252. package/dist/esm/utils/throttle.js +0 -34
  253. package/dist/esm/utils/throttle.js.map +0 -1
  254. package/dist/types/utils/throttle.d.ts +0 -9
  255. package/src/utils/throttle.test.ts +0 -141
  256. package/src/utils/throttle.ts +0 -34
@@ -27,7 +27,10 @@ internal object FaroCrashTraceCache {
27
27
  * Returns a cached trace without clearing it so multiple ApplicationExitInfo
28
28
  * rows for the same crash can share the UncaughtExceptionHandler stack.
29
29
  */
30
- fun peekPendingCrashTrace(context: Context): PendingTrace? {
30
+ fun peekPendingCrashTrace(
31
+ context: Context,
32
+ nowMs: Long = System.currentTimeMillis(),
33
+ ): PendingTrace? {
31
34
  val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
32
35
  val trace = prefs.getString(KEY_TRACE, null)?.trim().orEmpty()
33
36
  val cachedTimestamp = prefs.getLong(KEY_TIMESTAMP, 0L)
@@ -35,6 +38,13 @@ internal object FaroCrashTraceCache {
35
38
  if (trace.isEmpty() || cachedTimestamp == 0L) {
36
39
  return null
37
40
  }
41
+ if (
42
+ cachedTimestamp > nowMs ||
43
+ nowMs - cachedTimestamp > FaroCrashSessionStore.MAX_CONTEXT_AGE_MS
44
+ ) {
45
+ clearPendingCrashTrace(context)
46
+ return null
47
+ }
38
48
 
39
49
  return PendingTrace(trace, cachedTimestamp)
40
50
  }
@@ -45,7 +55,6 @@ internal object FaroCrashTraceCache {
45
55
  }
46
56
  val delta = kotlin.math.abs(exitTimestampMs - pending.timestampMs)
47
57
  if (delta > MAX_TIMESTAMP_DELTA_MS) {
48
- clearPendingCrashTrace(context)
49
58
  return ""
50
59
  }
51
60
  return pending.trace
@@ -39,9 +39,39 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
39
39
  }
40
40
 
41
41
  private var anrTracker: ANRTracker? = null
42
+ private val sessionPersistenceOwner = Any()
42
43
 
43
44
  override fun getName(): String = NAME
44
45
 
46
+ /** Stable process name used to isolate persisted session chains. */
47
+ @ReactMethod(isBlockingSynchronousMethod = true)
48
+ fun getSessionProcessIdentifier(): String? {
49
+ return FaroSessionProcess.identifier()
50
+ }
51
+
52
+ /** Whether this module is running in the application's declared main process. */
53
+ @ReactMethod(isBlockingSynchronousMethod = true)
54
+ fun isMainSessionProcess(): Boolean? {
55
+ return FaroSessionProcess.isMainProcess(reactApplicationContext.applicationContext)
56
+ }
57
+
58
+ /** Allows only one React Native runtime in this process to write session state. */
59
+ @ReactMethod(isBlockingSynchronousMethod = true)
60
+ fun claimSessionPersistence(): Boolean {
61
+ return FaroSessionProcess.claimPersistence(sessionPersistenceOwner)
62
+ }
63
+
64
+ /** Releases session persistence when this React Native runtime is torn down. */
65
+ @ReactMethod(isBlockingSynchronousMethod = true)
66
+ fun releaseSessionPersistence(): Boolean {
67
+ return FaroSessionProcess.releasePersistence(sessionPersistenceOwner)
68
+ }
69
+
70
+ override fun invalidate() {
71
+ FaroSessionProcess.releasePersistence(sessionPersistenceOwner)
72
+ super.invalidate()
73
+ }
74
+
45
75
  /**
46
76
  * Gets app startup duration in milliseconds using Android OS APIs
47
77
  *
@@ -96,9 +126,9 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
96
126
  */
97
127
  @ReactMethod
98
128
  fun startFrameMonitoring(config: ReadableMap) {
99
- val targetFps = if (config.hasKey("targetFps")) config.getDouble("targetFps") else 60.0
100
- val frozenFrameThresholdMs = if (config.hasKey("frozenFrameThresholdMs")) config.getDouble("frozenFrameThresholdMs") else 100.0
101
- val normalizedRefreshRate = if (config.hasKey("normalizedRefreshRate")) config.getDouble("normalizedRefreshRate") else 60.0
129
+ val targetFps = if (config.hasKey("targetFps")) config.getDouble("targetFps") else FrameMonitor.DEFAULT_TARGET_FPS
130
+ val frozenFrameThresholdMs = if (config.hasKey("frozenFrameThresholdMs")) config.getDouble("frozenFrameThresholdMs") else FrameMonitor.DEFAULT_FROZEN_FRAME_THRESHOLD_MS
131
+ val normalizedRefreshRate = if (config.hasKey("normalizedRefreshRate")) config.getDouble("normalizedRefreshRate") else FrameMonitor.DEFAULT_NORMALIZED_REFRESH_RATE
102
132
 
103
133
  FrameMonitor.configure(
104
134
  targetFps = targetFps,
@@ -106,17 +136,8 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
106
136
  normalizedRefreshRate = normalizedRefreshRate
107
137
  )
108
138
 
109
- // Set up event callbacks to emit events to JavaScript
139
+ // Refresh rate events only; slow/frozen frames are read via getFrameMetrics polling
110
140
  FrameMonitor.setCallbacks(
111
- onSlowFrames = { count -> sendEvent("onSlowFrames", count) },
112
- onFrozenFrame = { count, durationMs ->
113
- // Send frozen frame event with count and duration
114
- val data = WritableNativeMap().apply {
115
- putInt("count", count)
116
- putDouble("durationMs", durationMs)
117
- }
118
- sendEvent("onFrozenFrame", data)
119
- },
120
141
  onRefreshRate = { rate -> sendEvent("onRefreshRate", rate) }
121
142
  )
122
143
 
@@ -154,11 +175,12 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
154
175
  */
155
176
  @ReactMethod
156
177
  fun getFrameMetrics(promise: Promise) {
178
+ val frozenMetrics = FrameMonitor.getAndResetFrozenMetrics()
157
179
  val metrics = WritableNativeMap().apply {
158
180
  putDouble("refreshRate", FrameMonitor.getRefreshRate())
159
181
  putInt("slowFrames", FrameMonitor.getAndResetSlowFrames())
160
- putInt("frozenFrames", FrameMonitor.getAndResetFrozenFrames())
161
- putDouble("frozenDurationMs", FrameMonitor.getAndResetFrozenDuration())
182
+ putInt("frozenFrames", frozenMetrics.count)
183
+ putDouble("frozenDurationMs", frozenMetrics.durationMs)
162
184
  }
163
185
  promise.resolve(metrics)
164
186
  }
@@ -298,10 +320,39 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
298
320
  */
299
321
  @ReactMethod
300
322
  fun getCrashReport(promise: Promise) {
323
+ resolveCrashReports(promise, acknowledgeImmediately = true)
324
+ }
325
+
326
+ /**
327
+ * Returns recovered crashes without marking them as processed. JavaScript
328
+ * must acknowledge each report after it has been handled.
329
+ */
330
+ @ReactMethod
331
+ fun getPendingCrashReports(promise: Promise) {
332
+ resolveCrashReports(promise, acknowledgeImmediately = false)
333
+ }
334
+
335
+ private fun resolveCrashReports(
336
+ promise: Promise,
337
+ acknowledgeImmediately: Boolean,
338
+ ) {
301
339
  val crashReports = FaroCrashReporter.getCrashReports(reactApplicationContext)
302
-
340
+
303
341
  if (crashReports != null) {
304
- val writableArray = com.facebook.react.bridge.Arguments.createArray()
342
+ if (acknowledgeImmediately) {
343
+ // Preserve eager acknowledgement for older JavaScript bundles
344
+ // that use the legacy bridge and cannot acknowledge per report.
345
+ val reportIds = crashReports.mapNotNull { report ->
346
+ try {
347
+ org.json.JSONObject(report).optString("reportId").trim().takeIf(String::isNotEmpty)
348
+ } catch (_: Exception) {
349
+ null
350
+ }
351
+ }
352
+ FaroCrashReporter.acknowledgeCrashReports(reactApplicationContext, reportIds)
353
+ }
354
+
355
+ val writableArray = Arguments.createArray()
305
356
  for (report in crashReports) {
306
357
  writableArray.pushString(report)
307
358
  }
@@ -311,6 +362,59 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
311
362
  }
312
363
  }
313
364
 
365
+ /**
366
+ * Persists the active Faro session independently of optional sticky-session storage.
367
+ * The synchronous write makes the context available even if the process crashes
368
+ * immediately after JavaScript observes a session change.
369
+ */
370
+ @ReactMethod(isBlockingSynchronousMethod = true)
371
+ fun recordCrashSessionContext(sessionContext: ReadableMap): Boolean {
372
+ return try {
373
+ val sessionId = sessionContext.getOptionalString("sessionId") ?: return false
374
+ val activatedAtMs = if (sessionContext.hasKey("activatedAt") && !sessionContext.isNull("activatedAt")) {
375
+ sessionContext.getDouble("activatedAt").toLong()
376
+ } else {
377
+ System.currentTimeMillis()
378
+ }
379
+ val isSampled = if (sessionContext.hasKey("isSampled") && !sessionContext.isNull("isSampled")) {
380
+ sessionContext.getBoolean("isSampled")
381
+ } else {
382
+ null
383
+ }
384
+
385
+ FaroUncaughtExceptionHandler.install(reactApplicationContext)
386
+ FaroCrashSessionStore.recordSessionContext(
387
+ context = reactApplicationContext,
388
+ sessionId = sessionId,
389
+ activatedAtMs = activatedAtMs,
390
+ isSampled = isSampled,
391
+ appVersion = sessionContext.getOptionalString("appVersion"),
392
+ appRelease = sessionContext.getOptionalString("appRelease"),
393
+ appBundleId = sessionContext.getOptionalString("appBundleId"),
394
+ )
395
+ } catch (_: Exception) {
396
+ false
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Acknowledges only reports that JavaScript has finished handling.
402
+ */
403
+ @ReactMethod
404
+ fun acknowledgeCrashReports(reportIds: ReadableArray, promise: Promise) {
405
+ val ids = mutableListOf<String>()
406
+ for (index in 0 until reportIds.size()) {
407
+ if (!reportIds.isNull(index)) {
408
+ reportIds.getString(index)?.trim()?.takeIf(String::isNotEmpty)?.let(ids::add)
409
+ }
410
+ }
411
+ if (FaroCrashReporter.acknowledgeCrashReports(reactApplicationContext, ids)) {
412
+ promise.resolve(null)
413
+ } else {
414
+ promise.reject("E_CRASH_ACK_FAILED", "Failed to persist recovered crash acknowledgement")
415
+ }
416
+ }
417
+
314
418
  /**
315
419
  * Required for NativeEventEmitter on modern React Native versions.
316
420
  */
@@ -336,4 +440,11 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
336
440
  // Ignore errors when sending events
337
441
  }
338
442
  }
443
+
444
+ private fun ReadableMap.getOptionalString(key: String): String? {
445
+ if (!hasKey(key) || isNull(key)) {
446
+ return null
447
+ }
448
+ return getString(key)?.trim()?.takeIf(String::isNotEmpty)
449
+ }
339
450
  }
@@ -0,0 +1,88 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.app.Application
4
+ import android.content.Context
5
+ import android.os.Build
6
+ import java.io.File
7
+ import java.lang.ref.WeakReference
8
+
9
+ /** Process identity and single-writer ownership for persisted session state. */
10
+ internal object FaroSessionProcess {
11
+ private val persistenceLock = Any()
12
+ private var persistenceOwner: WeakReference<Any>? = null
13
+
14
+ fun identifier(): String? =
15
+ identifier(
16
+ sdkInt = Build.VERSION.SDK_INT,
17
+ currentProcessName = { Application.getProcessName() },
18
+ legacyProcessName = { File("/proc/self/cmdline").readText() },
19
+ )
20
+
21
+ internal fun identifier(
22
+ sdkInt: Int,
23
+ currentProcessName: () -> String?,
24
+ legacyProcessName: () -> String?,
25
+ ): String? {
26
+ val currentName = try {
27
+ if (sdkInt >= Build.VERSION_CODES.P) {
28
+ currentProcessName()
29
+ } else {
30
+ legacyProcessName()?.substringBefore('\u0000')
31
+ }
32
+ } catch (_: Exception) {
33
+ null
34
+ }
35
+
36
+ return normalizeIdentifier(currentName)
37
+ }
38
+
39
+ fun isMainProcess(context: Context): Boolean? {
40
+ return isMainProcess(
41
+ currentName = identifier(),
42
+ declaredMainName = context.applicationInfo.processName,
43
+ packageName = context.packageName,
44
+ )
45
+ }
46
+
47
+ fun claimPersistence(owner: Any): Boolean =
48
+ synchronized(persistenceLock) {
49
+ val currentOwner = persistenceOwner?.get()
50
+ if (currentOwner != null && currentOwner !== owner) {
51
+ false
52
+ } else {
53
+ persistenceOwner = WeakReference(owner)
54
+ true
55
+ }
56
+ }
57
+
58
+ fun releasePersistence(owner: Any): Boolean =
59
+ synchronized(persistenceLock) {
60
+ if (persistenceOwner?.get() !== owner) {
61
+ false
62
+ } else {
63
+ persistenceOwner = null
64
+ true
65
+ }
66
+ }
67
+
68
+ internal fun normalizeIdentifier(currentName: String?): String? =
69
+ currentName?.trim()?.takeIf { it.isNotEmpty() }
70
+
71
+ internal fun isMainProcess(
72
+ currentName: String?,
73
+ declaredMainName: String?,
74
+ packageName: String?,
75
+ ): Boolean? {
76
+ val normalizedCurrentName = normalizeIdentifier(currentName) ?: return null
77
+ val normalizedMainName = normalizeIdentifier(declaredMainName)
78
+ ?: normalizeIdentifier(packageName)
79
+ ?: return null
80
+ return normalizedCurrentName == normalizedMainName
81
+ }
82
+
83
+ internal fun resetPersistenceClaimForTest() {
84
+ synchronized(persistenceLock) {
85
+ persistenceOwner = null
86
+ }
87
+ }
88
+ }
@@ -21,20 +21,27 @@ object FrameMonitor {
21
21
  private const val NANOSECONDS_IN_MILLISECOND = 1_000_000L
22
22
 
23
23
  // Throttle refresh rate emits to avoid flooding the JS bridge (was ~60/sec).
24
- // Aligns with refreshRatePollingInterval (30s) on iOS and Flutter SDK.
24
+ // Aligns with refreshRatePollingInterval (30s) on iOS.
25
25
  private const val REFRESH_RATE_EMIT_INTERVAL_MS = 30_000L
26
26
 
27
+ const val DEFAULT_TARGET_FPS = 60.0
28
+ const val DEFAULT_FROZEN_FRAME_THRESHOLD_MS = 700.0
29
+ const val DEFAULT_NORMALIZED_REFRESH_RATE = 60.0
30
+
31
+ data class FrozenFrameMetrics(val count: Int, val durationMs: Double)
32
+
27
33
  // Configuration
28
- private var targetFps: Double = 60.0
29
- private var frozenFrameThresholdNs: Long = 100 * NANOSECONDS_IN_MILLISECOND
30
- private var normalizedRefreshRate: Double = 60.0
34
+ private var targetFps: Double = DEFAULT_TARGET_FPS
35
+ private var frozenFrameThresholdNs: Long =
36
+ (DEFAULT_FROZEN_FRAME_THRESHOLD_MS * NANOSECONDS_IN_MILLISECOND).toLong()
37
+ private var normalizedRefreshRate: Double = DEFAULT_NORMALIZED_REFRESH_RATE
31
38
 
32
39
  // State
33
40
  private var lastFrameTimeNanos: Long = 0
34
41
  private var lastRefreshRate: Double = 0.0
35
42
  private var lastRefreshRateEmitTimeMs: Long = 0
36
43
  private val slowFrameEventCount = AtomicInteger(0)
37
- private val frozenFrameCount = AtomicInteger(0)
44
+ private var frozenFrameCount: Int = 0
38
45
  private var frozenFrameDurationMs: Double = 0.0
39
46
  private val isMonitoring = AtomicBoolean(false)
40
47
 
@@ -54,13 +61,13 @@ object FrameMonitor {
54
61
  * Configure monitoring parameters.
55
62
  *
56
63
  * @param targetFps Target FPS for slow frame detection (default 60)
57
- * @param frozenFrameThresholdMs Threshold in ms for frozen frames (default 100)
64
+ * @param frozenFrameThresholdMs Threshold in ms for frozen frames (default 700)
58
65
  * @param normalizedRefreshRate Normalized rate for high-refresh displays (default 60)
59
66
  */
60
67
  fun configure(
61
- targetFps: Double = 60.0,
62
- frozenFrameThresholdMs: Double = 100.0,
63
- normalizedRefreshRate: Double = 60.0
68
+ targetFps: Double = DEFAULT_TARGET_FPS,
69
+ frozenFrameThresholdMs: Double = DEFAULT_FROZEN_FRAME_THRESHOLD_MS,
70
+ normalizedRefreshRate: Double = DEFAULT_NORMALIZED_REFRESH_RATE
64
71
  ) {
65
72
  this.targetFps = targetFps
66
73
  this.frozenFrameThresholdNs = (frozenFrameThresholdMs * NANOSECONDS_IN_MILLISECOND).toLong()
@@ -91,7 +98,7 @@ object FrameMonitor {
91
98
  lastFrameTimeNanos = 0
92
99
  lastRefreshRateEmitTimeMs = 0
93
100
  slowFrameEventCount.set(0)
94
- frozenFrameCount.set(0)
101
+ frozenFrameCount = 0
95
102
  frozenFrameDurationMs = 0.0
96
103
  inSlowFrameEvent = false
97
104
  slowFrameEventStartTimeNanos = 0
@@ -131,7 +138,7 @@ object FrameMonitor {
131
138
  // Reset state
132
139
  lastFrameTimeNanos = 0
133
140
  slowFrameEventCount.set(0)
134
- frozenFrameCount.set(0)
141
+ frozenFrameCount = 0
135
142
  frozenFrameDurationMs = 0.0
136
143
  }
137
144
 
@@ -151,20 +158,22 @@ object FrameMonitor {
151
158
  }
152
159
 
153
160
  /**
154
- * Get and reset frozen frame count.
155
- */
156
- fun getAndResetFrozenFrames(): Int {
157
- return frozenFrameCount.getAndSet(0)
158
- }
159
-
160
- /**
161
- * Get and reset frozen frame duration in milliseconds.
161
+ * Get and reset frozen frame count and duration atomically.
162
162
  */
163
163
  @Synchronized
164
- fun getAndResetFrozenDuration(): Double {
164
+ fun getAndResetFrozenMetrics(): FrozenFrameMetrics {
165
+ val count = frozenFrameCount
165
166
  val duration = frozenFrameDurationMs
167
+ frozenFrameCount = 0
166
168
  frozenFrameDurationMs = 0.0
167
- return duration
169
+ return FrozenFrameMetrics(count, duration)
170
+ }
171
+
172
+ @Synchronized
173
+ private fun recordFrozenFrame(durationMs: Double) {
174
+ frozenFrameCount += 1
175
+ frozenFrameDurationMs += durationMs
176
+ onFrozenFrame?.invoke(frozenFrameCount, durationMs)
168
177
  }
169
178
 
170
179
  /**
@@ -202,10 +211,7 @@ object FrameMonitor {
202
211
  // A slow frame "event" is a period of consecutive frames below target FPS
203
212
  // This groups consecutive slow frames to report meaningful jank, not microsecond variations
204
213
  val isSlow = fps < targetFps
205
- if (isSlow) {
206
- android.util.Log.d(TAG, "[Faro DEBUG ANDROID] 🐌 Slow frame detected: %.1f FPS (target: %.1f)".format(fps, targetFps))
207
- }
208
-
214
+
209
215
  if (isSlow) {
210
216
  if (!inSlowFrameEvent) {
211
217
  // Start new slow frame event
@@ -216,18 +222,11 @@ object FrameMonitor {
216
222
  // Frame is good - check if we should end the current slow frame event
217
223
  if (inSlowFrameEvent) {
218
224
  val eventDurationNs = frameTimeNanos - slowFrameEventStartTimeNanos
219
- val eventDurationMs = eventDurationNs / NANOSECONDS_IN_MILLISECOND
220
- val thresholdMs = slowFrameEventMinDurationNs / NANOSECONDS_IN_MILLISECOND
221
-
225
+
222
226
  // Only count as a slow frame event if it lasted long enough to be user-perceptible
223
227
  // This filters out single-frame dips that don't affect user experience
224
228
  if (eventDurationNs >= slowFrameEventMinDurationNs) {
225
- val newCount = slowFrameEventCount.incrementAndGet()
226
- // 🔍 TEMP DEBUG LOG - Remove after analysis
227
- android.util.Log.d(TAG, "[Faro DEBUG ANDROID] ✅ COUNTED as event (${eventDurationMs}ms)! Total events now: $newCount")
228
- } else {
229
- // 🔍 TEMP DEBUG LOG - Remove after analysis
230
- android.util.Log.d(TAG, "[Faro DEBUG ANDROID] ❌ NOT counted (${eventDurationMs}ms is too short). Total events still: ${slowFrameEventCount.get()}")
229
+ slowFrameEventCount.incrementAndGet()
231
230
  }
232
231
 
233
232
  inSlowFrameEvent = false
@@ -237,15 +236,8 @@ object FrameMonitor {
237
236
 
238
237
  // Check for frozen frames (frame duration exceeds threshold)
239
238
  if (frameDuration > frozenFrameThresholdNs) {
240
- val count = frozenFrameCount.incrementAndGet()
241
239
  val durationMs = frameDuration.toDouble() / NANOSECONDS_IN_MILLISECOND
242
-
243
- // Track duration (synchronized to avoid race conditions)
244
- synchronized(this) {
245
- frozenFrameDurationMs += durationMs
246
- }
247
-
248
- onFrozenFrame?.invoke(count, durationMs)
240
+ recordFrozenFrame(durationMs)
249
241
  }
250
242
 
251
243
  lastFrameTimeNanos = frameTimeNanos
@@ -0,0 +1,21 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.content.Context
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import org.junit.Assert.assertNull
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import org.robolectric.RobolectricTestRunner
9
+ import org.robolectric.annotation.Config
10
+
11
+ @RunWith(RobolectricTestRunner::class)
12
+ @Config(manifest = Config.NONE, sdk = [29])
13
+ class FaroCrashReporterBelowApi30Test {
14
+
15
+ @Test
16
+ fun getCrashReports_isNoOpBeforeApi30() {
17
+ val context = ApplicationProvider.getApplicationContext<Context>()
18
+
19
+ assertNull(FaroCrashReporter.getCrashReports(context))
20
+ }
21
+ }
@@ -0,0 +1,59 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertFalse
5
+ import org.junit.Assert.assertNotEquals
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ class FaroCrashReporterTest {
10
+
11
+ @Test
12
+ fun reportId_isStableForSameProcessCrash() {
13
+ val first = FaroCrashIds.reportId(
14
+ packageName = "com.example.app",
15
+ timestampMs = 1_000L,
16
+ pid = 101,
17
+ processName = "com.example.app",
18
+ )
19
+ val second = FaroCrashIds.reportId(
20
+ packageName = "com.example.app",
21
+ timestampMs = 1_000L,
22
+ pid = 101,
23
+ processName = "com.example.app",
24
+ )
25
+
26
+ assertEquals(first, second)
27
+ assertTrue(first.startsWith("v1:"))
28
+ assertEquals(67, first.length)
29
+ }
30
+
31
+ @Test
32
+ fun reportId_distinguishesProcessesAtSameTimestamp() {
33
+ val first = FaroCrashIds.reportId(
34
+ packageName = "com.example.app",
35
+ timestampMs = 1_000L,
36
+ pid = 101,
37
+ processName = "com.example.app",
38
+ )
39
+ val second = FaroCrashIds.reportId(
40
+ packageName = "com.example.app",
41
+ timestampMs = 1_000L,
42
+ pid = 202,
43
+ processName = "com.example.app:worker",
44
+ )
45
+
46
+ assertNotEquals(first, second)
47
+ }
48
+
49
+ @Test
50
+ fun replayWindow_includesExactSevenDayBoundary() {
51
+ val nowMs = 1_000_000_000L
52
+ val sevenDays = FaroCrashSessionStore.MAX_CONTEXT_AGE_MS
53
+
54
+ assertTrue(FaroCrashReporter.isWithinReplayWindow(nowMs - sevenDays, nowMs))
55
+ assertFalse(FaroCrashReporter.isWithinReplayWindow(nowMs - sevenDays - 1L, nowMs))
56
+ assertFalse(FaroCrashReporter.isWithinReplayWindow(0L, nowMs))
57
+ assertFalse(FaroCrashReporter.isWithinReplayWindow(nowMs + 1L, nowMs))
58
+ }
59
+ }