@grafana/faro-react-native 1.2.1 → 1.3.1

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 (217) hide show
  1. package/android/build.gradle +118 -36
  2. package/android/consumer-proguard-rules.pro +2 -0
  3. package/android/src/main/java/com/grafana/faro/reactnative/ANRTracker.kt +61 -9
  4. package/android/src/main/java/com/grafana/faro/reactnative/AnrThreadDumpParser.kt +70 -0
  5. package/android/src/main/java/com/grafana/faro/reactnative/ApplicationExitTraceReader.kt +142 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrCache.kt +163 -0
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrReporter.kt +163 -0
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +243 -50
  9. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +75 -0
  10. package/android/src/main/java/com/grafana/faro/reactnative/FaroNativeCrashTrace.kt +39 -0
  11. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +99 -8
  12. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativePackage.kt +1 -0
  13. package/android/src/main/java/com/grafana/faro/reactnative/FaroUncaughtExceptionHandler.kt +62 -0
  14. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneBacktraceFormatter.kt +117 -0
  15. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneModels.kt +35 -0
  16. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneWireParser.kt +235 -0
  17. package/android/src/main/proto/tombstone.proto +62 -0
  18. package/android/src/test/java/com/grafana/faro/reactnative/AnrStackFiltersTest.kt +28 -0
  19. package/android/src/test/java/com/grafana/faro/reactnative/AnrThreadDumpParserTest.kt +38 -0
  20. package/android/src/test/java/com/grafana/faro/reactnative/FaroAnrCacheTest.kt +81 -0
  21. package/android/src/test/java/com/grafana/faro/reactnative/TombstoneWireParserTest.kt +51 -0
  22. package/dist/cjs/config/getRNInstrumentations.js +3 -1
  23. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  24. package/dist/cjs/config/makeRNConfig.js +48 -9
  25. package/dist/cjs/config/makeRNConfig.js.map +1 -1
  26. package/dist/cjs/config/types.js.map +1 -1
  27. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  28. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  29. package/dist/cjs/index.js +3 -2
  30. package/dist/cjs/index.js.map +1 -1
  31. package/dist/cjs/initialize.js +14 -8
  32. package/dist/cjs/initialize.js.map +1 -1
  33. package/dist/cjs/instrumentations/anr/anrStack.js +19 -0
  34. package/dist/cjs/instrumentations/anr/anrStack.js.map +1 -0
  35. package/dist/cjs/instrumentations/anr/instrumentation.js +374 -72
  36. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/anr/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/console/index.js +5 -1
  39. package/dist/cjs/instrumentations/console/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +110 -103
  41. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
  42. package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +41 -0
  43. package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
  44. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +87 -0
  45. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
  46. package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js +123 -0
  47. package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
  48. package/dist/cjs/instrumentations/crashReporting/android/index.js +12 -0
  49. package/dist/cjs/instrumentations/crashReporting/android/index.js.map +1 -0
  50. package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js +11 -0
  51. package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
  52. package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +181 -0
  53. package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
  54. package/dist/cjs/instrumentations/crashReporting/index.js +35 -2
  55. package/dist/cjs/instrumentations/crashReporting/index.js.map +1 -1
  56. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +78 -0
  57. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
  58. package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js +103 -0
  59. package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
  60. package/dist/cjs/instrumentations/crashReporting/ios/index.js +11 -0
  61. package/dist/cjs/instrumentations/crashReporting/ios/index.js.map +1 -0
  62. package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js +48 -0
  63. package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
  64. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  65. package/dist/cjs/instrumentations/errors/index.js +2 -1
  66. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  67. package/dist/cjs/instrumentations/session/installationId.js +91 -0
  68. package/dist/cjs/instrumentations/session/installationId.js.map +1 -0
  69. package/dist/cjs/instrumentations/session/sessionAttributes.js +152 -89
  70. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  71. package/dist/cjs/metas/appBuildIdentity.js +95 -0
  72. package/dist/cjs/metas/appBuildIdentity.js.map +1 -0
  73. package/dist/cjs/testUtils/mockDeviceInfo.js +5 -0
  74. package/dist/cjs/testUtils/mockDeviceInfo.js.map +1 -1
  75. package/dist/cjs/testUtils/setupTests.js +7 -0
  76. package/dist/cjs/testUtils/setupTests.js.map +1 -1
  77. package/dist/esm/config/getRNInstrumentations.js +3 -1
  78. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  79. package/dist/esm/config/makeRNConfig.js +49 -10
  80. package/dist/esm/config/makeRNConfig.js.map +1 -1
  81. package/dist/esm/config/types.js.map +1 -1
  82. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  83. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  84. package/dist/esm/index.js +1 -1
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/initialize.js +11 -5
  87. package/dist/esm/initialize.js.map +1 -1
  88. package/dist/esm/instrumentations/anr/anrStack.js +16 -0
  89. package/dist/esm/instrumentations/anr/anrStack.js.map +1 -0
  90. package/dist/esm/instrumentations/anr/instrumentation.js +260 -67
  91. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  92. package/dist/esm/instrumentations/anr/types.js.map +1 -1
  93. package/dist/esm/instrumentations/console/index.js +5 -1
  94. package/dist/esm/instrumentations/console/index.js.map +1 -1
  95. package/dist/esm/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +76 -94
  96. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
  97. package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +18 -0
  98. package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
  99. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +56 -0
  100. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
  101. package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js +116 -0
  102. package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
  103. package/dist/esm/instrumentations/crashReporting/android/index.js +4 -0
  104. package/dist/esm/instrumentations/crashReporting/android/index.js.map +1 -0
  105. package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js +8 -0
  106. package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
  107. package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +174 -0
  108. package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
  109. package/dist/esm/instrumentations/crashReporting/index.js +35 -1
  110. package/dist/esm/instrumentations/crashReporting/index.js.map +1 -1
  111. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +56 -0
  112. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
  113. package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js +98 -0
  114. package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
  115. package/dist/esm/instrumentations/crashReporting/ios/index.js +4 -0
  116. package/dist/esm/instrumentations/crashReporting/ios/index.js.map +1 -0
  117. package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js +44 -0
  118. package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
  119. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  120. package/dist/esm/instrumentations/errors/index.js +2 -1
  121. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  122. package/dist/esm/instrumentations/session/installationId.js +48 -0
  123. package/dist/esm/instrumentations/session/installationId.js.map +1 -0
  124. package/dist/esm/instrumentations/session/sessionAttributes.js +83 -38
  125. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  126. package/dist/esm/metas/appBuildIdentity.js +56 -0
  127. package/dist/esm/metas/appBuildIdentity.js.map +1 -0
  128. package/dist/esm/testUtils/mockDeviceInfo.js +5 -0
  129. package/dist/esm/testUtils/mockDeviceInfo.js.map +1 -1
  130. package/dist/esm/testUtils/setupTests.js +7 -0
  131. package/dist/esm/testUtils/setupTests.js.map +1 -1
  132. package/dist/types/config/makeRNConfig.d.ts +4 -3
  133. package/dist/types/config/types.d.ts +2 -1
  134. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  135. package/dist/types/index.d.ts +2 -2
  136. package/dist/types/initialize.d.ts +4 -2
  137. package/dist/types/instrumentations/anr/anrStack.d.ts +7 -0
  138. package/dist/types/instrumentations/anr/instrumentation.d.ts +23 -28
  139. package/dist/types/instrumentations/anr/types.d.ts +5 -1
  140. package/dist/types/instrumentations/appState/index.d.ts +1 -1
  141. package/dist/types/instrumentations/console/index.d.ts +1 -1
  142. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +56 -0
  143. package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +12 -0
  144. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +26 -0
  145. package/dist/types/instrumentations/crashReporting/android/crashErrorMessage.d.ts +23 -0
  146. package/dist/types/instrumentations/crashReporting/android/index.d.ts +4 -0
  147. package/dist/types/instrumentations/crashReporting/android/nativeTombstoneTrace.d.ts +2 -0
  148. package/dist/types/instrumentations/crashReporting/android/parseAndroidCrashTrace.d.ts +45 -0
  149. package/dist/types/instrumentations/crashReporting/index.d.ts +25 -2
  150. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +28 -0
  151. package/dist/types/instrumentations/crashReporting/ios/crashErrorMessage.d.ts +14 -0
  152. package/dist/types/instrumentations/crashReporting/ios/index.d.ts +4 -0
  153. package/dist/types/instrumentations/crashReporting/ios/parseIosCrashTrace.d.ts +38 -0
  154. package/dist/types/instrumentations/crashReporting/types.d.ts +18 -51
  155. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +1 -1
  156. package/dist/types/instrumentations/http/index.d.ts +1 -1
  157. package/dist/types/instrumentations/performance/index.d.ts +1 -1
  158. package/dist/types/instrumentations/session/index.d.ts +1 -1
  159. package/dist/types/instrumentations/session/installationId.d.ts +1 -0
  160. package/dist/types/instrumentations/session/sessionAttributes.d.ts +13 -4
  161. package/dist/types/instrumentations/startup/index.d.ts +1 -1
  162. package/dist/types/instrumentations/userActions/index.d.ts +1 -1
  163. package/dist/types/instrumentations/view/index.d.ts +1 -1
  164. package/dist/types/instrumentations/xhr/index.d.ts +1 -1
  165. package/dist/types/metas/appBuildIdentity.d.ts +16 -0
  166. package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
  167. package/dist/types/transports/console/transport.d.ts +1 -1
  168. package/dist/types/transports/fetch/transport.d.ts +1 -1
  169. package/dist/types/transports/offline/transport.d.ts +1 -1
  170. package/package.json +4 -4
  171. package/src/config/getRNInstrumentations.ts +5 -1
  172. package/src/config/makeRNConfig.test.ts +59 -3
  173. package/src/config/makeRNConfig.ts +48 -9
  174. package/src/config/types.ts +2 -1
  175. package/src/generated/faroRNPackageMeta.ts +1 -1
  176. package/src/index.ts +2 -1
  177. package/src/initialize.test.ts +71 -15
  178. package/src/initialize.ts +11 -5
  179. package/src/instrumentations/anr/anrStack.test.ts +27 -0
  180. package/src/instrumentations/anr/anrStack.ts +19 -0
  181. package/src/instrumentations/anr/instrumentation.test.ts +45 -0
  182. package/src/instrumentations/anr/instrumentation.ts +301 -67
  183. package/src/instrumentations/anr/types.ts +7 -1
  184. package/src/instrumentations/console/index.ts +5 -1
  185. package/src/instrumentations/console/instrumentation.test.ts +5 -0
  186. package/src/instrumentations/crashReporting/{instrumentation.ts → BaseCrashReportingInstrumentation.ts} +119 -93
  187. package/src/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.ts +23 -0
  188. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +62 -0
  189. package/src/instrumentations/crashReporting/android/crashErrorMessage.test.ts +182 -0
  190. package/src/instrumentations/crashReporting/android/crashErrorMessage.ts +135 -0
  191. package/src/instrumentations/crashReporting/android/index.ts +4 -0
  192. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.test.ts +19 -0
  193. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.ts +8 -0
  194. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.test.ts +232 -0
  195. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.ts +235 -0
  196. package/src/instrumentations/crashReporting/index.ts +43 -2
  197. package/src/instrumentations/crashReporting/instrumentation.test.ts +42 -0
  198. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +62 -0
  199. package/src/instrumentations/crashReporting/ios/crashErrorMessage.test.ts +170 -0
  200. package/src/instrumentations/crashReporting/ios/crashErrorMessage.ts +111 -0
  201. package/src/instrumentations/crashReporting/ios/index.ts +4 -0
  202. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.test.ts +86 -0
  203. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.ts +83 -0
  204. package/src/instrumentations/crashReporting/types.ts +19 -58
  205. package/src/instrumentations/errors/index.ts +2 -1
  206. package/src/instrumentations/errors/instrumentation.test.ts +8 -2
  207. package/src/instrumentations/errors/stackTraceParser.test.ts +1 -1
  208. package/src/instrumentations/session/installationId.ts +43 -0
  209. package/src/instrumentations/session/sessionAttributes.test.ts +260 -30
  210. package/src/instrumentations/session/sessionAttributes.ts +112 -22
  211. package/src/metas/appBuildIdentity.test.ts +48 -0
  212. package/src/metas/appBuildIdentity.ts +50 -0
  213. package/src/testUtils/mockDeviceInfo.ts +5 -0
  214. package/src/testUtils/setupTests.ts +7 -0
  215. package/dist/cjs/instrumentations/crashReporting/instrumentation.js.map +0 -1
  216. package/dist/esm/instrumentations/crashReporting/instrumentation.js.map +0 -1
  217. package/dist/types/instrumentations/crashReporting/instrumentation.d.ts +0 -58
@@ -0,0 +1,163 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import org.json.JSONArray
6
+ import org.json.JSONObject
7
+
8
+ /**
9
+ * Persists pending ANR payloads between detection and JS delivery.
10
+ *
11
+ * ANRs are written with [commit] on the ANR tracker thread as soon as the 5s
12
+ * threshold fires, so a process kill before the React bridge can emit still
13
+ * leaves a recoverable record for the next launch.
14
+ */
15
+ internal object FaroAnrCache {
16
+ private const val PREFS_NAME = "com.grafana.faro.anr_cache"
17
+ private const val KEY_PENDING_ANRS = "pending_anrs"
18
+ private const val KEY_DETECTED_ANR_TIMESTAMPS = "detected_anr_timestamps"
19
+ private const val MAX_PENDING_ENTRIES = 50
20
+ private const val MAX_DETECTED_TIMESTAMP_ENTRIES = 50
21
+ /** Match window between ANRTracker detection time and ApplicationExitInfo exit time. */
22
+ private const val MAX_TIMESTAMP_DELTA_MS = 10_000L
23
+
24
+ fun savePendingAnr(context: Context, payload: String) {
25
+ val newObj = try {
26
+ JSONObject(payload)
27
+ } catch (_: Exception) {
28
+ return
29
+ }
30
+ val newTimestamp = newObj.optLong("timestamp", 0L)
31
+ if (newTimestamp == 0L) {
32
+ return
33
+ }
34
+
35
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
36
+ val array = JSONArray(prefs.getString(KEY_PENDING_ANRS, "[]") ?: "[]")
37
+
38
+ for (i in 0 until array.length()) {
39
+ val existing = array.optJSONObject(i) ?: continue
40
+ val existingTimestamp = existing.optLong("timestamp", 0L)
41
+ if (existingTimestamp == newTimestamp) {
42
+ return
43
+ }
44
+ if (isWithinDetectionWindow(existingTimestamp, newTimestamp)) {
45
+ // Collapse repeated tracker detections for the same blocked period.
46
+ return
47
+ }
48
+ }
49
+
50
+ array.put(newObj)
51
+ trimOldestEntries(array)
52
+
53
+ recordDetectionTimestamp(prefs, newTimestamp)
54
+
55
+ // commit() not apply(): the process may be killed immediately after ANR detection.
56
+ prefs.edit()
57
+ .putString(KEY_PENDING_ANRS, array.toString())
58
+ .commit()
59
+ }
60
+
61
+ /**
62
+ * Returns true when Faro already recorded an ANR near [exitTimestampMs].
63
+ * Used to skip ApplicationExitInfo crash replays of the same blocked-main-thread event.
64
+ */
65
+ fun hasNearbyAnrDetection(context: Context, exitTimestampMs: Long): Boolean {
66
+ if (exitTimestampMs <= 0L) {
67
+ return false
68
+ }
69
+
70
+ for (payload in getPendingAnrs(context)) {
71
+ val timestamp = try {
72
+ JSONObject(payload).optLong("timestamp", 0L)
73
+ } catch (_: Exception) {
74
+ 0L
75
+ }
76
+ if (isWithinDetectionWindow(timestamp, exitTimestampMs)) {
77
+ return true
78
+ }
79
+ }
80
+
81
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
82
+ val detected = JSONArray(prefs.getString(KEY_DETECTED_ANR_TIMESTAMPS, "[]") ?: "[]")
83
+ for (i in 0 until detected.length()) {
84
+ val timestamp = detected.optLong(i, 0L)
85
+ if (isWithinDetectionWindow(timestamp, exitTimestampMs)) {
86
+ return true
87
+ }
88
+ }
89
+
90
+ return false
91
+ }
92
+
93
+ private fun recordDetectionTimestamp(prefs: SharedPreferences, timestampMs: Long) {
94
+ val detected = JSONArray(prefs.getString(KEY_DETECTED_ANR_TIMESTAMPS, "[]") ?: "[]")
95
+ for (i in 0 until detected.length()) {
96
+ if (detected.optLong(i, 0L) == timestampMs) {
97
+ return
98
+ }
99
+ }
100
+ detected.put(timestampMs)
101
+ while (detected.length() > MAX_DETECTED_TIMESTAMP_ENTRIES) {
102
+ detected.remove(0)
103
+ }
104
+ prefs.edit()
105
+ .putString(KEY_DETECTED_ANR_TIMESTAMPS, detected.toString())
106
+ .commit()
107
+ }
108
+
109
+ private fun isWithinDetectionWindow(anrTimestampMs: Long, exitTimestampMs: Long): Boolean {
110
+ if (anrTimestampMs <= 0L) {
111
+ return false
112
+ }
113
+ return kotlin.math.abs(exitTimestampMs - anrTimestampMs) <= MAX_TIMESTAMP_DELTA_MS
114
+ }
115
+
116
+ fun getPendingAnrs(context: Context): List<String> {
117
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
118
+ val array = JSONArray(prefs.getString(KEY_PENDING_ANRS, "[]") ?: "[]")
119
+ val result = ArrayList<String>(array.length())
120
+ for (i in 0 until array.length()) {
121
+ val obj = array.optJSONObject(i) ?: continue
122
+ result.add(obj.toString())
123
+ }
124
+ return result
125
+ }
126
+
127
+ /** Records an ANR exit timestamp so [FaroCrashReporter] skips duplicate crash rows. */
128
+ fun recordDetectionTimestamp(context: Context, timestampMs: Long) {
129
+ if (timestampMs <= 0L) {
130
+ return
131
+ }
132
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
133
+ recordDetectionTimestamp(prefs, timestampMs)
134
+ }
135
+
136
+ fun acknowledgeAnrs(context: Context, timestamps: Collection<Long>) {
137
+ if (timestamps.isEmpty()) {
138
+ return
139
+ }
140
+
141
+ val timestampSet = timestamps.toSet()
142
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
143
+ val array = JSONArray(prefs.getString(KEY_PENDING_ANRS, "[]") ?: "[]")
144
+ val remaining = JSONArray()
145
+
146
+ for (i in 0 until array.length()) {
147
+ val obj = array.optJSONObject(i) ?: continue
148
+ if (obj.optLong("timestamp", 0L) !in timestampSet) {
149
+ remaining.put(obj)
150
+ }
151
+ }
152
+
153
+ prefs.edit()
154
+ .putString(KEY_PENDING_ANRS, remaining.toString())
155
+ .commit()
156
+ }
157
+
158
+ private fun trimOldestEntries(array: JSONArray) {
159
+ while (array.length() > MAX_PENDING_ENTRIES) {
160
+ array.remove(0)
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,163 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.app.ActivityManager
4
+ import android.app.ApplicationExitInfo
5
+ import android.content.Context
6
+ import android.os.Build
7
+ import org.json.JSONObject
8
+ import java.io.BufferedReader
9
+ import java.io.InputStreamReader
10
+
11
+ /**
12
+ * Reports ANRs from ApplicationExitInfo (Android 11+), Sentry AnrV2 style.
13
+ *
14
+ * Kept separate from [FaroCrashReporter] so ANR timeout rows are not replayed as
15
+ * generic previous-session `crash` events (no message / duplicate rows).
16
+ */
17
+ object FaroAnrReporter {
18
+
19
+ private const val PREFS_NAME = "com.grafana.faro.anr_reporter"
20
+ private const val KEY_LAST_PROCESSED_TIMESTAMP = "last_processed_anr_timestamp"
21
+
22
+ @JvmStatic
23
+ fun getAnrReports(context: Context): List<String>? {
24
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
25
+ return null
26
+ }
27
+
28
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
29
+ ?: return null
30
+
31
+ val exitInfoList = activityManager.getHistoricalProcessExitReasons(
32
+ context.packageName,
33
+ 0,
34
+ 10,
35
+ )
36
+
37
+ if (exitInfoList.isEmpty()) {
38
+ return null
39
+ }
40
+
41
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
42
+ val lastProcessedTimestamp = prefs.getLong(KEY_LAST_PROCESSED_TIMESTAMP, 0)
43
+
44
+ val reports = mutableListOf<String>()
45
+ var latestTimestamp = lastProcessedTimestamp
46
+
47
+ val anrExits = exitInfoList
48
+ .filter { it.timestamp > lastProcessedTimestamp && it.reason == ApplicationExitInfo.REASON_ANR }
49
+ .groupBy { it.timestamp }
50
+ .mapValues { (_, exits) -> pickBestAnrExit(exits) }
51
+ .toSortedMap()
52
+
53
+ for ((timestamp, exitInfo) in anrExits) {
54
+ if (exitInfo.timestamp > latestTimestamp) {
55
+ latestTimestamp = exitInfo.timestamp
56
+ }
57
+
58
+ val json = exportAnrAsJSON(context, exitInfo) ?: continue
59
+ reports.add(json)
60
+ }
61
+
62
+ if (latestTimestamp > lastProcessedTimestamp) {
63
+ prefs.edit()
64
+ .putLong(KEY_LAST_PROCESSED_TIMESTAMP, latestTimestamp)
65
+ .apply()
66
+ }
67
+
68
+ return if (reports.isEmpty()) null else reports
69
+ }
70
+
71
+ private fun pickBestAnrExit(exits: List<ApplicationExitInfo>): ApplicationExitInfo {
72
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
73
+ return exits.first()
74
+ }
75
+
76
+ return exits.maxByOrNull { exit ->
77
+ var score = 0
78
+ if (getTraceInfo(exit).isNotEmpty()) {
79
+ score += 2
80
+ }
81
+ val description = exit.description?.trim().orEmpty()
82
+ if (description.isNotEmpty()) {
83
+ score += 1
84
+ }
85
+ score
86
+ } ?: exits.first()
87
+ }
88
+
89
+ private fun exportAnrAsJSON(context: Context, exitInfo: ApplicationExitInfo): String? {
90
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
91
+ return null
92
+ }
93
+
94
+ val fullTrace = getTraceInfo(exitInfo)
95
+ if (fullTrace.isEmpty()) {
96
+ // Align with Sentry: skip ANRs without an actionable trace stream.
97
+ return null
98
+ }
99
+
100
+ val mainStack = AnrThreadDumpParser.extractMainThreadStack(fullTrace)
101
+ if (mainStack.isEmpty() || AnrStackFilters.isCrashHandlerStack(mainStack)) {
102
+ return null
103
+ }
104
+
105
+ val rawDescription = exitInfo.description?.trim().orEmpty()
106
+ val description = rawDescription.ifEmpty { "Application Not Responding" }
107
+
108
+ FaroAnrCache.recordDetectionTimestamp(context, exitInfo.timestamp)
109
+
110
+ // duration is the configured watchdog threshold (set via startANRTracking), not the
111
+ // measured main-thread block time — AppExitInfo does not expose exact freeze duration.
112
+ // TODO: consider renaming the JSON field to detectionThresholdMs for clarity.
113
+ return JSONObject().apply {
114
+ put("type", "ANR")
115
+ put("timestamp", exitInfo.timestamp)
116
+ put("duration", ANRTracker.timeout)
117
+ put("stacktrace", mainStack)
118
+ put("description", description)
119
+ put("source", "AppExitInfo")
120
+ put("processName", exitInfo.processName ?: "")
121
+ put("pid", exitInfo.pid)
122
+ }.toString()
123
+ }
124
+
125
+ private fun getTraceInfo(exitInfo: ApplicationExitInfo): String {
126
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
127
+ return ""
128
+ }
129
+
130
+ return try {
131
+ val traceInputStream = exitInfo.traceInputStream ?: return ""
132
+ val reader = BufferedReader(InputStreamReader(traceInputStream))
133
+ val trace = StringBuilder()
134
+ var lineCount = 0
135
+ val maxLines = 500
136
+
137
+ reader.useLines { lines ->
138
+ for (line in lines) {
139
+ if (lineCount >= maxLines) break
140
+ trace.appendLine(line)
141
+ lineCount++
142
+ }
143
+ }
144
+
145
+ trace.toString().trim()
146
+ } catch (_: Exception) {
147
+ ""
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Reject watchdog snapshots taken while the process is tearing down after a crash.
154
+ * Uses only Android framework and Faro SDK frame names.
155
+ */
156
+ internal object AnrStackFilters {
157
+ fun isCrashHandlerStack(stack: String): Boolean {
158
+ val normalized = stack.lowercase()
159
+ return normalized.contains("handleapplicationcrash") ||
160
+ normalized.contains("killapplicationhandler") ||
161
+ normalized.contains("farouncaughtexceptionhandler")
162
+ }
163
+ }