@grafana/faro-react-native 1.2.0 → 1.3.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 (218) 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/ios/faro-upload-composed-source-map.sh +8 -2
  171. package/package.json +5 -5
  172. package/src/config/getRNInstrumentations.ts +5 -1
  173. package/src/config/makeRNConfig.test.ts +59 -3
  174. package/src/config/makeRNConfig.ts +48 -9
  175. package/src/config/types.ts +2 -1
  176. package/src/generated/faroRNPackageMeta.ts +1 -1
  177. package/src/index.ts +2 -1
  178. package/src/initialize.test.ts +71 -15
  179. package/src/initialize.ts +11 -5
  180. package/src/instrumentations/anr/anrStack.test.ts +27 -0
  181. package/src/instrumentations/anr/anrStack.ts +19 -0
  182. package/src/instrumentations/anr/instrumentation.test.ts +45 -0
  183. package/src/instrumentations/anr/instrumentation.ts +301 -67
  184. package/src/instrumentations/anr/types.ts +7 -1
  185. package/src/instrumentations/console/index.ts +5 -1
  186. package/src/instrumentations/console/instrumentation.test.ts +5 -0
  187. package/src/instrumentations/crashReporting/{instrumentation.ts → BaseCrashReportingInstrumentation.ts} +119 -93
  188. package/src/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.ts +23 -0
  189. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +62 -0
  190. package/src/instrumentations/crashReporting/android/crashErrorMessage.test.ts +182 -0
  191. package/src/instrumentations/crashReporting/android/crashErrorMessage.ts +135 -0
  192. package/src/instrumentations/crashReporting/android/index.ts +4 -0
  193. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.test.ts +19 -0
  194. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.ts +8 -0
  195. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.test.ts +232 -0
  196. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.ts +235 -0
  197. package/src/instrumentations/crashReporting/index.ts +43 -2
  198. package/src/instrumentations/crashReporting/instrumentation.test.ts +42 -0
  199. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +62 -0
  200. package/src/instrumentations/crashReporting/ios/crashErrorMessage.test.ts +170 -0
  201. package/src/instrumentations/crashReporting/ios/crashErrorMessage.ts +111 -0
  202. package/src/instrumentations/crashReporting/ios/index.ts +4 -0
  203. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.test.ts +86 -0
  204. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.ts +83 -0
  205. package/src/instrumentations/crashReporting/types.ts +19 -58
  206. package/src/instrumentations/errors/index.ts +2 -1
  207. package/src/instrumentations/errors/instrumentation.test.ts +8 -2
  208. package/src/instrumentations/errors/stackTraceParser.test.ts +1 -1
  209. package/src/instrumentations/session/installationId.ts +43 -0
  210. package/src/instrumentations/session/sessionAttributes.test.ts +260 -30
  211. package/src/instrumentations/session/sessionAttributes.ts +112 -22
  212. package/src/metas/appBuildIdentity.test.ts +48 -0
  213. package/src/metas/appBuildIdentity.ts +50 -0
  214. package/src/testUtils/mockDeviceInfo.ts +5 -0
  215. package/src/testUtils/setupTests.ts +7 -0
  216. package/dist/cjs/instrumentations/crashReporting/instrumentation.js.map +0 -1
  217. package/dist/esm/instrumentations/crashReporting/instrumentation.js.map +0 -1
  218. package/dist/types/instrumentations/crashReporting/instrumentation.d.ts +0 -58
@@ -4,9 +4,8 @@ import android.app.ActivityManager
4
4
  import android.app.ApplicationExitInfo
5
5
  import android.content.Context
6
6
  import android.os.Build
7
+ import android.util.Log
7
8
  import org.json.JSONObject
8
- import java.io.BufferedReader
9
- import java.io.InputStreamReader
10
9
 
11
10
  /**
12
11
  * Android Crash Reporter using ApplicationExitInfo API (Android 11+).
@@ -23,7 +22,6 @@ import java.io.InputStreamReader
23
22
  * ## Supported Exit Reasons
24
23
  * - REASON_CRASH: Java/Kotlin exception crash
25
24
  * - REASON_CRASH_NATIVE: Native (NDK) crash
26
- * - REASON_ANR: Application Not Responding
27
25
  * - REASON_LOW_MEMORY: Killed due to low memory
28
26
  * - REASON_EXCESSIVE_RESOURCE_USAGE: Killed due to excessive resource usage
29
27
  *
@@ -36,6 +34,17 @@ object FaroCrashReporter {
36
34
  private const val PREFS_NAME = "com.grafana.faro.crash_reporter"
37
35
  private const val KEY_LAST_PROCESSED_TIMESTAMP = "last_processed_timestamp"
38
36
 
37
+ /**
38
+ * Persists a text tombstone backtrace for the next [REASON_CRASH_NATIVE] replay.
39
+ *
40
+ * Use when [ApplicationExitInfo.getTraceInputStream] is null (common on emulators)
41
+ * and the app can capture a native backtrace immediately before a fatal native exit.
42
+ */
43
+ @JvmStatic
44
+ fun cachePendingNativeCrashTrace(context: Context, trace: String) {
45
+ FaroNativeCrashTrace.cachePendingNativeCrashTrace(context, trace)
46
+ }
47
+
39
48
  /**
40
49
  * Gets crash reports from previous sessions as JSON strings.
41
50
  *
@@ -48,6 +57,7 @@ object FaroCrashReporter {
48
57
  */
49
58
  @JvmStatic
50
59
  fun getCrashReports(context: Context): List<String>? {
60
+ FaroUncaughtExceptionHandler.install(context)
51
61
  // ApplicationExitInfo requires Android 11 (API 30)
52
62
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
53
63
  return null
@@ -70,32 +80,47 @@ object FaroCrashReporter {
70
80
  val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
71
81
  val lastProcessedTimestamp = prefs.getLong(KEY_LAST_PROCESSED_TIMESTAMP, 0)
72
82
 
83
+ val pendingTrace = FaroCrashTraceCache.peekPendingCrashTrace(context)
84
+ var usedPendingTrace = false
85
+
73
86
  val crashReports = mutableListOf<String>()
74
87
  var latestTimestamp = lastProcessedTimestamp
75
-
76
- for (exitInfo in exitInfoList) {
77
- // Skip already processed entries
78
- if (exitInfo.timestamp <= lastProcessedTimestamp) {
79
- continue
80
- }
81
-
82
- // Only process crash-related exit reasons
83
- if (!isCrashReason(exitInfo.reason)) {
88
+ val reportedTimestamps = mutableSetOf<Long>()
89
+
90
+ // Android can return multiple ApplicationExitInfo rows for one fatal exit
91
+ // (e.g. REASON_CRASH plus a companion row with no trace). Collapse to one
92
+ // report per timestamp and share the UncaughtExceptionHandler cache.
93
+ val candidates = exitInfoList
94
+ .filter { it.timestamp > lastProcessedTimestamp && isCrashReason(it.reason) }
95
+ .groupBy { it.timestamp }
96
+ .mapValues { (_, exits) -> pickBestExitInfo(exits) }
97
+ .toSortedMap()
98
+
99
+ for ((timestamp, exitInfo) in candidates) {
100
+ if (reportedTimestamps.contains(timestamp)) {
84
101
  continue
85
102
  }
86
103
 
87
- // Track the latest timestamp
88
104
  if (exitInfo.timestamp > latestTimestamp) {
89
105
  latestTimestamp = exitInfo.timestamp
90
106
  }
91
107
 
92
- // Convert to JSON
93
- val jsonString = exportExitInfoAsJSON(exitInfo)
108
+ val jsonString = exportExitInfoAsJSON(context, exitInfo, pendingTrace) { consumed ->
109
+ if (consumed) {
110
+ usedPendingTrace = true
111
+ }
112
+ }
113
+
94
114
  if (jsonString != null) {
95
115
  crashReports.add(jsonString)
116
+ reportedTimestamps.add(timestamp)
96
117
  }
97
118
  }
98
119
 
120
+ if (usedPendingTrace) {
121
+ FaroCrashTraceCache.clearPendingCrashTrace(context)
122
+ }
123
+
99
124
  // Update the last processed timestamp
100
125
  if (latestTimestamp > lastProcessedTimestamp) {
101
126
  prefs.edit()
@@ -107,7 +132,41 @@ object FaroCrashReporter {
107
132
  }
108
133
 
109
134
  /**
110
- * Check if the exit reason indicates a crash or ANR.
135
+ * Prefer the row that already carries a trace stream; otherwise keep the first entry.
136
+ */
137
+ private fun pickBestExitInfo(exits: List<ApplicationExitInfo>): ApplicationExitInfo {
138
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
139
+ return exits.first()
140
+ }
141
+
142
+ return exits.maxByOrNull { exit ->
143
+ var score = 0
144
+ if (exit.reason == ApplicationExitInfo.REASON_CRASH) {
145
+ score += 4
146
+ } else if (exit.reason == ApplicationExitInfo.REASON_CRASH_NATIVE) {
147
+ score += 3
148
+ }
149
+ val trace = readExitTrace(exit).text
150
+ if (trace.isNotEmpty()) {
151
+ score += 2
152
+ }
153
+ if (TombstoneBacktraceFormatter.looksLikeNativeBacktrace(trace)) {
154
+ score += 3
155
+ }
156
+ if (hasExceptionHeader(trace)) {
157
+ score += 2
158
+ }
159
+ val description = exit.description?.trim().orEmpty()
160
+ if (description.isNotEmpty()) {
161
+ score += 1
162
+ }
163
+ score
164
+ } ?: exits.first()
165
+ }
166
+
167
+ /**
168
+ * Check if the exit reason should be replayed as a previous-session crash.
169
+ * ANRs are excluded: ANRInstrumentation reports them with the blocked main-thread stack.
111
170
  */
112
171
  private fun isCrashReason(reason: Int): Boolean {
113
172
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
@@ -117,7 +176,6 @@ object FaroCrashReporter {
117
176
  return when (reason) {
118
177
  ApplicationExitInfo.REASON_CRASH,
119
178
  ApplicationExitInfo.REASON_CRASH_NATIVE,
120
- ApplicationExitInfo.REASON_ANR,
121
179
  ApplicationExitInfo.REASON_LOW_MEMORY,
122
180
  ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> true
123
181
  else -> false
@@ -127,7 +185,12 @@ object FaroCrashReporter {
127
185
  /**
128
186
  * Convert ApplicationExitInfo to JSON string matching iOS format.
129
187
  */
130
- private fun exportExitInfoAsJSON(exitInfo: ApplicationExitInfo): String? {
188
+ private fun exportExitInfoAsJSON(
189
+ context: Context,
190
+ exitInfo: ApplicationExitInfo,
191
+ pendingTrace: FaroCrashTraceCache.PendingTrace?,
192
+ onPendingTraceUsed: (Boolean) -> Unit,
193
+ ): String? {
131
194
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
132
195
  return null
133
196
  }
@@ -145,7 +208,10 @@ object FaroCrashReporter {
145
208
  json.put("status", exitInfo.status)
146
209
 
147
210
  // Description - human-readable description
148
- json.put("description", exitInfo.description ?: getDefaultDescription(exitInfo.reason))
211
+ val rawDescription = exitInfo.description?.trim().orEmpty()
212
+ val description = rawDescription.takeIf { it.isNotEmpty() && !isGenericDescription(it) }
213
+ ?: getDefaultDescription(exitInfo.reason)
214
+ json.put("description", description)
149
215
 
150
216
  // Process info
151
217
  json.put("processName", exitInfo.processName ?: "")
@@ -154,10 +220,57 @@ object FaroCrashReporter {
154
220
  // Importance (Android-specific)
155
221
  json.put("importance", exitInfo.importance)
156
222
 
157
- // Stack trace (if available)
158
- val trace = getTraceInfo(exitInfo)
223
+ if (isAnrTimeoutDescription(rawDescription)) {
224
+ // ANRInstrumentation reports these with type ANR.
225
+ return null
226
+ }
227
+
228
+ val parsedExitTrace = readExitTrace(exitInfo)
229
+ val cachedTrace = cachedTraceForExit(context, exitInfo, pendingTrace)
230
+ val trace = resolveCrashTrace(
231
+ exitTrace = parsedExitTrace.text,
232
+ cachedTrace = cachedTrace,
233
+ isNativeCrash = exitInfo.reason == ApplicationExitInfo.REASON_CRASH_NATIVE,
234
+ )
235
+
236
+ if (FaroAnrCache.hasNearbyAnrDetection(context, exitInfo.timestamp) &&
237
+ trace.isEmpty() &&
238
+ !hasMeaningfulDescription(exitInfo)
239
+ ) {
240
+ // Suppress empty generic crash rows that duplicate a nearby ANR report.
241
+ // Real crashes with a trace still surface even when an ANR happened nearby.
242
+ return null
243
+ }
244
+
159
245
  if (trace.isNotEmpty()) {
246
+ if (parsedExitTrace.text.isEmpty() && cachedTrace.isNotEmpty()) {
247
+ onPendingTraceUsed(true)
248
+ } else if (cachedTrace.isNotEmpty() && trace == cachedTrace) {
249
+ onPendingTraceUsed(true)
250
+ }
160
251
  json.put("trace", trace)
252
+
253
+ val signal = parsedExitTrace.signal.trim()
254
+ if (signal.isNotEmpty()) {
255
+ json.put("signal", signal)
256
+ }
257
+
258
+ if (TombstoneBacktraceFormatter.looksLikeNativeBacktrace(trace)) {
259
+ val preview = trace.lineSequence().firstOrNull { it.contains("#00 pc") }?.trim().orEmpty()
260
+ Log.i(
261
+ TAG,
262
+ "[Faro crash native] Exporting crash report with tombstone trace (${trace.lineSequence().count()} lines) preview=$preview",
263
+ )
264
+ }
265
+ } else if (!hasMeaningfulDescription(exitInfo)) {
266
+ // Skip duplicate/no-signal rows that would surface as generic "crash" in the UI.
267
+ if (exitInfo.reason == ApplicationExitInfo.REASON_CRASH_NATIVE) {
268
+ Log.w(
269
+ TAG,
270
+ "[Faro crash native] Skipping CRASH_NATIVE without tombstone trace (traceInputStream was null and no cached native backtrace)",
271
+ )
272
+ }
273
+ return null
161
274
  }
162
275
 
163
276
  json.toString()
@@ -166,6 +279,115 @@ object FaroCrashReporter {
166
279
  }
167
280
  }
168
281
 
282
+ private fun cachedTraceForExit(
283
+ context: Context,
284
+ exitInfo: ApplicationExitInfo,
285
+ pendingTrace: FaroCrashTraceCache.PendingTrace?,
286
+ ): String {
287
+ val cached = FaroCrashTraceCache.traceForExitTimestamp(context, pendingTrace, exitInfo.timestamp)
288
+ if (cached.isEmpty()) {
289
+ return ""
290
+ }
291
+
292
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
293
+ exitInfo.reason == ApplicationExitInfo.REASON_CRASH_NATIVE
294
+ ) {
295
+ cached.takeIf { TombstoneBacktraceFormatter.looksLikeNativeBacktrace(it) }.orEmpty()
296
+ } else {
297
+ cached
298
+ }
299
+ }
300
+
301
+ /**
302
+ * ApplicationExitInfo traces on emulators often contain only "at …" frame lines.
303
+ * UncaughtExceptionHandler cache includes the exception class + message header
304
+ * required for plugin titles (e.g. java.lang.NullPointerException).
305
+ */
306
+ private fun resolveCrashTrace(
307
+ exitTrace: String,
308
+ cachedTrace: String,
309
+ isNativeCrash: Boolean,
310
+ ): String {
311
+ val exit = exitTrace.trim()
312
+ val cached = cachedTrace.trim()
313
+
314
+ if (isNativeCrash) {
315
+ if (TombstoneBacktraceFormatter.looksLikeNativeBacktrace(exit)) {
316
+ return exit
317
+ }
318
+ if (TombstoneBacktraceFormatter.looksLikeNativeBacktrace(cached)) {
319
+ return cached
320
+ }
321
+ return exit.ifEmpty { cached }
322
+ }
323
+
324
+ if (exit.isEmpty()) {
325
+ return cached
326
+ }
327
+ if (cached.isEmpty()) {
328
+ return exit
329
+ }
330
+
331
+ val exitHasHeader = hasExceptionHeader(exit)
332
+ val cachedHasHeader = hasExceptionHeader(cached)
333
+
334
+ return when {
335
+ cachedHasHeader && !exitHasHeader -> cached
336
+ exitHasHeader && !cachedHasHeader -> exit
337
+ cached.length > exit.length -> cached
338
+ else -> exit
339
+ }
340
+ }
341
+
342
+ private fun readExitTrace(exitInfo: ApplicationExitInfo): ApplicationExitTraceReader.ParsedExitTrace {
343
+ return ApplicationExitTraceReader.read(exitInfo)
344
+ }
345
+
346
+ private fun isAnrTimeoutDescription(description: String): Boolean {
347
+ val normalized = description.trim().lowercase()
348
+ if (normalized.isEmpty()) {
349
+ return false
350
+ }
351
+ return normalized.contains("input dispatching timed out") ||
352
+ normalized.contains("not responding") ||
353
+ normalized.contains("application not responding")
354
+ }
355
+
356
+ private fun hasExceptionHeader(trace: String): Boolean {
357
+ return trace.lineSequence().any { line ->
358
+ val trimmed = line.trim()
359
+ trimmed.isNotEmpty() &&
360
+ !trimmed.startsWith("at ") &&
361
+ !trimmed.startsWith("#") &&
362
+ trimmed.contains('.') &&
363
+ !trimmed.startsWith("Caused by:")
364
+ }
365
+ }
366
+
367
+ private fun isGenericDescription(description: String): Boolean {
368
+ val normalized = description.trim().lowercase()
369
+ return normalized in setOf(
370
+ "crash",
371
+ "native crash",
372
+ "application crash",
373
+ "application crash (java/kotlin)",
374
+ "application crash (native)",
375
+ )
376
+ }
377
+
378
+ private fun hasMeaningfulDescription(exitInfo: ApplicationExitInfo): Boolean {
379
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
380
+ return false
381
+ }
382
+
383
+ val description = exitInfo.description?.trim().orEmpty()
384
+ if (description.isEmpty()) {
385
+ return false
386
+ }
387
+
388
+ return !isGenericDescription(description)
389
+ }
390
+
169
391
  /**
170
392
  * Get human-readable reason string from exit reason code.
171
393
  */
@@ -209,33 +431,4 @@ object FaroCrashReporter {
209
431
  else -> "Application crash"
210
432
  }
211
433
  }
212
-
213
- /**
214
- * Get stack trace info from ApplicationExitInfo.
215
- */
216
- private fun getTraceInfo(exitInfo: ApplicationExitInfo): String {
217
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
218
- return ""
219
- }
220
-
221
- return try {
222
- val traceInputStream = exitInfo.traceInputStream ?: return ""
223
- val reader = BufferedReader(InputStreamReader(traceInputStream))
224
- val trace = StringBuilder()
225
- var lineCount = 0
226
- val maxLines = 100 // Limit trace length
227
-
228
- reader.useLines { lines ->
229
- for (line in lines) {
230
- if (lineCount >= maxLines) break
231
- trace.appendLine(line)
232
- lineCount++
233
- }
234
- }
235
-
236
- trace.toString().trim()
237
- } catch (e: Exception) {
238
- ""
239
- }
240
- }
241
434
  }
@@ -0,0 +1,75 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.content.Context
4
+
5
+ /**
6
+ * Persists a pending native crash stack between process death and the next launch.
7
+ */
8
+ internal object FaroCrashTraceCache {
9
+ private const val PREFS_NAME = "com.grafana.faro.crash_trace_cache"
10
+ private const val KEY_TRACE = "pending_trace"
11
+ private const val KEY_TIMESTAMP = "pending_timestamp"
12
+ private const val MAX_TIMESTAMP_DELTA_MS = 10_000L
13
+
14
+ fun savePendingCrashTrace(context: Context, trace: String, timestampMs: Long) {
15
+ // commit() not apply(): the process is killed immediately after uncaughtException.
16
+ context.applicationContext
17
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
18
+ .edit()
19
+ .putString(KEY_TRACE, trace)
20
+ .putLong(KEY_TIMESTAMP, timestampMs)
21
+ .commit()
22
+ }
23
+
24
+ data class PendingTrace(val trace: String, val timestampMs: Long)
25
+
26
+ /**
27
+ * Returns a cached trace without clearing it so multiple ApplicationExitInfo
28
+ * rows for the same crash can share the UncaughtExceptionHandler stack.
29
+ */
30
+ fun peekPendingCrashTrace(context: Context): PendingTrace? {
31
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
32
+ val trace = prefs.getString(KEY_TRACE, null)?.trim().orEmpty()
33
+ val cachedTimestamp = prefs.getLong(KEY_TIMESTAMP, 0L)
34
+
35
+ if (trace.isEmpty() || cachedTimestamp == 0L) {
36
+ return null
37
+ }
38
+
39
+ return PendingTrace(trace, cachedTimestamp)
40
+ }
41
+
42
+ fun traceForExitTimestamp(context: Context, pending: PendingTrace?, exitTimestampMs: Long): String {
43
+ if (pending == null) {
44
+ return ""
45
+ }
46
+ val delta = kotlin.math.abs(exitTimestampMs - pending.timestampMs)
47
+ if (delta > MAX_TIMESTAMP_DELTA_MS) {
48
+ clearPendingCrashTrace(context)
49
+ return ""
50
+ }
51
+ return pending.trace
52
+ }
53
+
54
+ fun clearPendingCrashTrace(context: Context) {
55
+ context.applicationContext
56
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
57
+ .edit()
58
+ .remove(KEY_TRACE)
59
+ .remove(KEY_TIMESTAMP)
60
+ .apply()
61
+ }
62
+
63
+ /**
64
+ * @deprecated Prefer peek + clearPendingCrashTrace so one crash can backfill several exit rows.
65
+ */
66
+ fun consumePendingCrashTrace(context: Context, exitTimestampMs: Long): String? {
67
+ val pending = peekPendingCrashTrace(context) ?: return null
68
+ val trace = traceForExitTimestamp(context, pending, exitTimestampMs)
69
+ if (trace.isEmpty()) {
70
+ return null
71
+ }
72
+ clearPendingCrashTrace(context)
73
+ return trace
74
+ }
75
+ }
@@ -0,0 +1,39 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+
6
+ /**
7
+ * Public helpers for NDK crash reporting integrations.
8
+ *
9
+ * Apps can call [cachePendingNativeCrashTrace] immediately before an intentional native
10
+ * crash when ApplicationExitInfo.traceInputStream is unavailable (common on emulators).
11
+ * The cached tombstone text is attached on the next launch when the exit reason is
12
+ * REASON_CRASH_NATIVE.
13
+ */
14
+ object FaroNativeCrashTrace {
15
+
16
+ private const val TAG = "FaroCrashReporter"
17
+
18
+ /**
19
+ * Persists a text tombstone backtrace for the next ApplicationExitInfo replay.
20
+ *
21
+ * The trace must contain native frame lines (`#00 pc … libfoo.so`) so the Faro
22
+ * collector can run NDK retrace.
23
+ */
24
+ @JvmStatic
25
+ fun cachePendingNativeCrashTrace(context: Context, trace: String) {
26
+ val trimmed = trace.trim()
27
+ if (trimmed.isEmpty()) {
28
+ Log.w(TAG, "[Faro crash native] Ignoring empty pending native trace cache")
29
+ return
30
+ }
31
+ if (!TombstoneBacktraceFormatter.looksLikeNativeBacktrace(trimmed)) {
32
+ Log.w(TAG, "[Faro crash native] Ignoring pending native trace without #00 pc frames")
33
+ return
34
+ }
35
+
36
+ FaroCrashTraceCache.savePendingCrashTrace(context, trimmed, System.currentTimeMillis())
37
+ Log.i(TAG, "[Faro crash native] Cached pending native tombstone (${trimmed.lineSequence().count()} lines)")
38
+ }
39
+ }
@@ -3,10 +3,12 @@ package com.grafana.faro.reactnative
3
3
  import android.os.Build
4
4
  import android.os.Process
5
5
  import android.os.SystemClock
6
+ import com.facebook.react.bridge.Arguments
6
7
  import com.facebook.react.bridge.Promise
7
8
  import com.facebook.react.bridge.ReactApplicationContext
8
9
  import com.facebook.react.bridge.ReactContextBaseJavaModule
9
10
  import com.facebook.react.bridge.ReactMethod
11
+ import com.facebook.react.bridge.ReadableArray
10
12
  import com.facebook.react.bridge.ReadableMap
11
13
  import com.facebook.react.bridge.WritableNativeMap
12
14
  import com.facebook.react.modules.core.DeviceEventManagerModule
@@ -174,6 +176,14 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
174
176
  // Configure timeout
175
177
  val timeout = if (config.hasKey("timeout")) config.getDouble("timeout").toLong() else 5000L
176
178
  ANRTracker.timeout = timeout
179
+ ANRTracker.applicationContext = reactApplicationContext.applicationContext
180
+
181
+ ANRTracker.onAnrDetected = { payload ->
182
+ // Emit from the ANR tracker thread. runOnUiQueueThread would defer until the
183
+ // main looper runs — but during a debug ANR the UI thread is blocked, so the
184
+ // event would not reach JS until the freeze ends (and may be lost on app kill).
185
+ sendEvent("onANRDetected", payload)
186
+ }
177
187
 
178
188
  // Start new tracker
179
189
  anrTracker = ANRTracker()
@@ -185,10 +195,34 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
185
195
  */
186
196
  @ReactMethod
187
197
  fun stopANRTracking() {
198
+ ANRTracker.onAnrDetected = null
199
+ ANRTracker.applicationContext = null
188
200
  anrTracker?.stopTracking()
189
201
  anrTracker = null
190
202
  }
191
203
 
204
+ /**
205
+ * Returns unsent ANRs persisted on disk (peek; does not clear).
206
+ */
207
+ @ReactMethod
208
+ fun getPendingANRs(promise: Promise) {
209
+ promise.resolve(pendingAnrsWritableArray())
210
+ }
211
+
212
+ /**
213
+ * Removes ANRs from the pending cache after JS has reported them.
214
+ */
215
+ @ReactMethod
216
+ fun acknowledgeANRs(timestamps: ReadableArray, promise: Promise) {
217
+ val ackTimestamps = mutableListOf<Long>()
218
+ for (i in 0 until timestamps.size()) {
219
+ ackTimestamps.add(timestamps.getDouble(i).toLong())
220
+ }
221
+ FaroAnrCache.acknowledgeAnrs(reactApplicationContext, ackTimestamps)
222
+ ANRTracker.acknowledgeInMemory(ackTimestamps)
223
+ promise.resolve(null)
224
+ }
225
+
192
226
  /**
193
227
  * Get ANR status (list of detected ANRs)
194
228
  *
@@ -196,13 +230,57 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
196
230
  */
197
231
  @ReactMethod
198
232
  fun getANRStatus(promise: Promise) {
199
- val anrList = ANRTracker.getANRStatus()
200
- ANRTracker.resetANR()
201
-
202
- if (anrList != null) {
203
- val writableArray = com.facebook.react.bridge.Arguments.createArray()
204
- for (anr in anrList) {
205
- writableArray.pushString(anr)
233
+ promise.resolve(pendingAnrsWritableArray())
234
+ }
235
+
236
+ private fun pendingAnrsWritableArray(): com.facebook.react.bridge.WritableArray? {
237
+ val diskPending = FaroAnrCache.getPendingAnrs(reactApplicationContext)
238
+ val memoryPending = ANRTracker.getANRStatus().orEmpty()
239
+ val merged = mergePendingAnrs(diskPending, memoryPending)
240
+
241
+ if (merged.isEmpty()) {
242
+ return null
243
+ }
244
+
245
+ val writableArray = Arguments.createArray()
246
+ for (anr in merged) {
247
+ writableArray.pushString(anr)
248
+ }
249
+ return writableArray
250
+ }
251
+
252
+ private fun mergePendingAnrs(diskPending: List<String>, memoryPending: List<String>): List<String> {
253
+ val merged = ArrayList<String>(diskPending.size + memoryPending.size)
254
+ val seenTimestamps = mutableSetOf<Long>()
255
+
256
+ for (payload in diskPending + memoryPending) {
257
+ val timestamp = try {
258
+ org.json.JSONObject(payload).optLong("timestamp", 0L)
259
+ } catch (_: Exception) {
260
+ 0L
261
+ }
262
+ if (timestamp == 0L || timestamp in seenTimestamps) {
263
+ continue
264
+ }
265
+ seenTimestamps.add(timestamp)
266
+ merged.add(payload)
267
+ }
268
+
269
+ return merged
270
+ }
271
+
272
+ /**
273
+ * Historical ANRs from ApplicationExitInfo (Android 11+), Sentry AnrV2 style.
274
+ * Separate from [getCrashReport] so ANRs are not replayed as generic crashes.
275
+ */
276
+ @ReactMethod
277
+ fun getHistoricalAnrReports(promise: Promise) {
278
+ val anrReports = FaroAnrReporter.getAnrReports(reactApplicationContext)
279
+
280
+ if (anrReports != null) {
281
+ val writableArray = Arguments.createArray()
282
+ for (report in anrReports) {
283
+ writableArray.pushString(report)
206
284
  }
207
285
  promise.resolve(writableArray)
208
286
  } else {
@@ -214,7 +292,7 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
214
292
  * Get crash reports from previous app sessions
215
293
  *
216
294
  * Uses Android's ApplicationExitInfo API (Android 11+) to retrieve
217
- * crash and ANR information from previous sessions.
295
+ * crash information from previous sessions (ANRs excluded).
218
296
  *
219
297
  * @param promise Promise to resolve with the crash reports list
220
298
  */
@@ -233,6 +311,19 @@ class FaroReactNativeModule(reactContext: ReactApplicationContext) :
233
311
  }
234
312
  }
235
313
 
314
+ /**
315
+ * Required for NativeEventEmitter on modern React Native versions.
316
+ */
317
+ @ReactMethod
318
+ fun addListener(eventName: String) {
319
+ // Keep: required for RN event subscription bookkeeping.
320
+ }
321
+
322
+ @ReactMethod
323
+ fun removeListeners(count: Int) {
324
+ // Keep: required for RN event subscription bookkeeping.
325
+ }
326
+
236
327
  /**
237
328
  * Send an event to JavaScript
238
329
  */
@@ -10,6 +10,7 @@ import com.facebook.react.uimanager.ViewManager
10
10
  */
11
11
  class FaroReactNativePackage : ReactPackage {
12
12
  override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
13
+ FaroUncaughtExceptionHandler.install(reactContext)
13
14
  return listOf(FaroReactNativeModule(reactContext))
14
15
  }
15
16