@grafana/faro-react-native 1.2.1 → 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 (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 +3 -3
  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,62 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+
6
+ /**
7
+ * Captures Java/Kotlin stack traces for fatal crashes before the process exits.
8
+ *
9
+ * ApplicationExitInfo.traceInputStream is often null on emulators and some devices
10
+ * (observed on Android 16 / API 36). Without this handler the SDK cannot report
11
+ * native frames for retrace.
12
+ */
13
+ internal class FaroUncaughtExceptionHandler(
14
+ private val context: Context,
15
+ private val originalHandler: Thread.UncaughtExceptionHandler?,
16
+ ) : Thread.UncaughtExceptionHandler {
17
+
18
+ override fun uncaughtException(thread: Thread, throwable: Throwable) {
19
+ try {
20
+ val trace = Log.getStackTraceString(throwable).trim()
21
+ if (trace.isNotEmpty()) {
22
+ FaroCrashTraceCache.savePendingCrashTrace(context, trace, System.currentTimeMillis())
23
+ }
24
+ } catch (_: Throwable) {
25
+ // Never interfere with the original crash path.
26
+ } finally {
27
+ originalHandler?.uncaughtException(thread, throwable)
28
+ ?: run {
29
+ @Suppress("DEPRECATION")
30
+ android.os.Process.killProcess(android.os.Process.myPid())
31
+ System.exit(10)
32
+ }
33
+ }
34
+ }
35
+
36
+ companion object {
37
+ @Volatile
38
+ private var installed = false
39
+
40
+ @JvmStatic
41
+ fun install(context: Context) {
42
+ if (installed) {
43
+ return
44
+ }
45
+ synchronized(this) {
46
+ if (installed) {
47
+ return
48
+ }
49
+ val appContext = context.applicationContext
50
+ val current = Thread.getDefaultUncaughtExceptionHandler()
51
+ if (current is FaroUncaughtExceptionHandler) {
52
+ installed = true
53
+ return
54
+ }
55
+ Thread.setDefaultUncaughtExceptionHandler(
56
+ FaroUncaughtExceptionHandler(appContext, current)
57
+ )
58
+ installed = true
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,117 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ /**
4
+ * Converts Android tombstone data into the text backtrace format expected by the Faro
5
+ * collector's NDK retrace pipeline.
6
+ */
7
+ internal object TombstoneBacktraceFormatter {
8
+
9
+ private val nativeFrameLine = Regex("""^\s*#\d+\s+pc\s+(?:0x)?[0-9a-fA-F]+\s+\S+""")
10
+
11
+ fun looksLikeNativeBacktrace(text: String): Boolean {
12
+ if (text.isBlank()) {
13
+ return false
14
+ }
15
+ return text.lineSequence().any { nativeFrameLine.containsMatchIn(it) }
16
+ }
17
+
18
+ fun looksLikeTextTombstone(text: String): Boolean {
19
+ if (text.isBlank()) {
20
+ return false
21
+ }
22
+ if (looksLikeNativeBacktrace(text)) {
23
+ return true
24
+ }
25
+ return text.contains("*** *** ***") || text.contains("backtrace:")
26
+ }
27
+
28
+ fun format(tombstone: ParsedTombstoneData): FormattedTombstone {
29
+ val signalLabel = formatSignal(tombstone.signalName, tombstone.signalNumber)
30
+ val crashThread = tombstone.threads[tombstone.tid]
31
+ ?: tombstone.threads.values.firstOrNull { it.backtrace.isNotEmpty() }
32
+ val frames = crashThread?.backtrace.orEmpty()
33
+
34
+ val lines = mutableListOf<String>()
35
+ lines.add("*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***")
36
+ lines.add("ABI: '${tombstone.arch}'")
37
+ if (tombstone.pid > 0) {
38
+ lines.add("pid: ${tombstone.pid}, tid: ${tombstone.tid}, name: ${crashThread?.name.orEmpty()}")
39
+ }
40
+ if (signalLabel.isNotEmpty()) {
41
+ lines.add("signal $signalLabel")
42
+ }
43
+ if (tombstone.abortMessage.isNotBlank()) {
44
+ lines.add("Abort message: '${tombstone.abortMessage.trim()}'")
45
+ }
46
+ lines.add("backtrace:")
47
+
48
+ frames.forEachIndexed { index, frame ->
49
+ lines.add(formatFrameLine(index, frame, tombstone.memoryMappings))
50
+ }
51
+
52
+ return FormattedTombstone(
53
+ text = lines.joinToString("\n").trim(),
54
+ signal = signalLabel,
55
+ )
56
+ }
57
+
58
+ data class FormattedTombstone(
59
+ val text: String,
60
+ val signal: String,
61
+ )
62
+
63
+ private fun formatSignal(name: String, number: Int): String {
64
+ if (name.isBlank() && number == 0) {
65
+ return ""
66
+ }
67
+ val signalName = name.trim().ifEmpty { "UNKNOWN" }
68
+ return if (number != 0) {
69
+ "$signalName ($number)"
70
+ } else {
71
+ signalName
72
+ }
73
+ }
74
+
75
+ private fun formatFrameLine(
76
+ index: Int,
77
+ frame: ParsedBacktraceFrame,
78
+ mappings: List<ParsedMemoryMapping>,
79
+ ): String {
80
+ val pc = frame.pc
81
+ val pcHex = pc.toString(16).padStart(16, '0')
82
+ val library = resolveLibrary(frame, mappings)
83
+ val suffix = buildString {
84
+ if (frame.functionName.isNotBlank()) {
85
+ append(" (")
86
+ append(frame.functionName)
87
+ if (frame.functionOffset > 0) {
88
+ append('+')
89
+ append(frame.functionOffset)
90
+ }
91
+ append(')')
92
+ }
93
+ if (frame.buildId.isNotBlank()) {
94
+ append(" (BuildId: ")
95
+ append(frame.buildId.trim())
96
+ append(')')
97
+ }
98
+ }
99
+ return " #${String.format("%02d", index)} pc $pcHex $library$suffix"
100
+ }
101
+
102
+ private fun resolveLibrary(
103
+ frame: ParsedBacktraceFrame,
104
+ mappings: List<ParsedMemoryMapping>,
105
+ ): String {
106
+ if (frame.fileName.isNotBlank()) {
107
+ return frame.fileName.trim()
108
+ }
109
+
110
+ val mapping = mappings.firstOrNull { frame.pc >= it.beginAddress && frame.pc < it.endAddress }
111
+ if (mapping != null && mapping.mappingName.isNotBlank()) {
112
+ return mapping.mappingName
113
+ }
114
+
115
+ return "unknown"
116
+ }
117
+ }
@@ -0,0 +1,35 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ internal data class ParsedTombstoneData(
4
+ val arch: String = "unknown",
5
+ val pid: Int = 0,
6
+ val tid: Int = 0,
7
+ val signalName: String = "",
8
+ val signalNumber: Int = 0,
9
+ val abortMessage: String = "",
10
+ val threads: Map<Int, ParsedThreadData> = emptyMap(),
11
+ val memoryMappings: List<ParsedMemoryMapping> = emptyList(),
12
+ )
13
+
14
+ internal data class ParsedThreadData(
15
+ val id: Int = 0,
16
+ val name: String = "",
17
+ val backtrace: List<ParsedBacktraceFrame> = emptyList(),
18
+ )
19
+
20
+ internal data class ParsedBacktraceFrame(
21
+ val relPc: Long = 0L,
22
+ val pc: Long = 0L,
23
+ val functionName: String = "",
24
+ val functionOffset: Long = 0L,
25
+ val fileName: String = "",
26
+ val buildId: String = "",
27
+ )
28
+
29
+ internal data class ParsedMemoryMapping(
30
+ val beginAddress: Long = 0L,
31
+ val endAddress: Long = 0L,
32
+ val mappingName: String = "",
33
+ val buildId: String = "",
34
+ val loadBias: Long = 0L,
35
+ )
@@ -0,0 +1,235 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import com.google.protobuf.CodedInputStream
4
+ import com.google.protobuf.InvalidProtocolBufferException
5
+ import com.google.protobuf.WireFormat
6
+
7
+ /**
8
+ * Parses Android tombstone protobuf without generated [TombstoneProtos.Tombstone.parseFrom].
9
+ *
10
+ * Release builds (R8) can strip protobuf generated fields, breaking parseFrom even when the wire
11
+ * bytes are valid. This parser is R8-safe.
12
+ */
13
+ internal object TombstoneWireParser {
14
+
15
+ private val architectureLabels = mapOf(
16
+ 0 to "arm",
17
+ 1 to "arm64",
18
+ 2 to "x86",
19
+ 3 to "x86_64",
20
+ 4 to "riscv64",
21
+ 5 to "unknown",
22
+ )
23
+
24
+ fun parse(bytes: ByteArray): ParsedTombstoneData? {
25
+ if (bytes.isEmpty()) {
26
+ return null
27
+ }
28
+
29
+ parseMessage(bytes)?.let { parsed ->
30
+ if (parsed.threads.values.any { it.backtrace.isNotEmpty() }) {
31
+ return parsed
32
+ }
33
+ }
34
+
35
+ // Some Android builds wrap the tombstone in an outer message; scan nested payloads.
36
+ for (nested in extractLengthDelimitedMessages(bytes)) {
37
+ parseMessage(nested)?.let { parsed ->
38
+ if (parsed.threads.values.any { it.backtrace.isNotEmpty() }) {
39
+ return parsed
40
+ }
41
+ }
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ private fun parseMessage(bytes: ByteArray): ParsedTombstoneData? {
48
+ return try {
49
+ val input = CodedInputStream.newInstance(bytes)
50
+ var arch = "unknown"
51
+ var pid = 0
52
+ var tid = 0
53
+ var signalName = ""
54
+ var signalNumber = 0
55
+ var abortMessage = ""
56
+ val threads = linkedMapOf<Int, ParsedThreadData>()
57
+ val memoryMappings = mutableListOf<ParsedMemoryMapping>()
58
+
59
+ while (true) {
60
+ val tag = input.readTag()
61
+ if (tag == 0) {
62
+ break
63
+ }
64
+
65
+ when (WireFormat.getTagFieldNumber(tag)) {
66
+ 1 -> arch = architectureLabels[input.readEnum()] ?: "unknown"
67
+ 5 -> pid = input.readUInt32()
68
+ 6 -> tid = input.readUInt32()
69
+ 10 -> parseSignal(input.readBytes().toByteArray())?.let { signal ->
70
+ signalName = signal.first
71
+ signalNumber = signal.second
72
+ }
73
+ 14 -> abortMessage = input.readStringRequireUtf8()
74
+ 16 -> parseThreadMapEntry(input.readBytes().toByteArray())?.let { (threadId, thread) ->
75
+ threads[threadId] = thread
76
+ }
77
+ 17 -> memoryMappings.add(parseMemoryMapping(input.readBytes().toByteArray()))
78
+ else -> input.skipField(tag)
79
+ }
80
+ }
81
+
82
+ ParsedTombstoneData(
83
+ arch = arch,
84
+ pid = pid,
85
+ tid = tid,
86
+ signalName = signalName,
87
+ signalNumber = signalNumber,
88
+ abortMessage = abortMessage,
89
+ threads = threads,
90
+ memoryMappings = memoryMappings,
91
+ )
92
+ } catch (_: InvalidProtocolBufferException) {
93
+ null
94
+ } catch (_: IndexOutOfBoundsException) {
95
+ null
96
+ }
97
+ }
98
+
99
+ private fun parseSignal(bytes: ByteArray): Pair<String, Int>? {
100
+ val input = CodedInputStream.newInstance(bytes)
101
+ var number = 0
102
+ var name = ""
103
+ while (true) {
104
+ val tag = input.readTag()
105
+ if (tag == 0) {
106
+ break
107
+ }
108
+ when (WireFormat.getTagFieldNumber(tag)) {
109
+ 1 -> number = input.readInt32()
110
+ 2 -> name = input.readStringRequireUtf8()
111
+ else -> input.skipField(tag)
112
+ }
113
+ }
114
+ return name to number
115
+ }
116
+
117
+ private fun parseThreadMapEntry(bytes: ByteArray): Pair<Int, ParsedThreadData>? {
118
+ val input = CodedInputStream.newInstance(bytes)
119
+ var threadId = 0
120
+ var threadBytes: ByteArray? = null
121
+ while (true) {
122
+ val tag = input.readTag()
123
+ if (tag == 0) {
124
+ break
125
+ }
126
+ when (WireFormat.getTagFieldNumber(tag)) {
127
+ 1 -> threadId = input.readUInt32()
128
+ 2 -> threadBytes = input.readBytes().toByteArray()
129
+ else -> input.skipField(tag)
130
+ }
131
+ }
132
+ val thread = threadBytes?.let { parseThread(it) } ?: return null
133
+ return threadId to thread
134
+ }
135
+
136
+ private fun parseThread(bytes: ByteArray): ParsedThreadData {
137
+ val input = CodedInputStream.newInstance(bytes)
138
+ var id = 0
139
+ var name = ""
140
+ val backtrace = mutableListOf<ParsedBacktraceFrame>()
141
+ while (true) {
142
+ val tag = input.readTag()
143
+ if (tag == 0) {
144
+ break
145
+ }
146
+ when (WireFormat.getTagFieldNumber(tag)) {
147
+ 1 -> id = input.readInt32()
148
+ 2 -> name = input.readStringRequireUtf8()
149
+ 4 -> backtrace.add(parseBacktraceFrame(input.readBytes().toByteArray()))
150
+ else -> input.skipField(tag)
151
+ }
152
+ }
153
+ return ParsedThreadData(id = id, name = name, backtrace = backtrace)
154
+ }
155
+
156
+ private fun parseBacktraceFrame(bytes: ByteArray): ParsedBacktraceFrame {
157
+ val input = CodedInputStream.newInstance(bytes)
158
+ var relPc = 0L
159
+ var pc = 0L
160
+ var functionName = ""
161
+ var functionOffset = 0L
162
+ var fileName = ""
163
+ var buildId = ""
164
+ while (true) {
165
+ val tag = input.readTag()
166
+ if (tag == 0) {
167
+ break
168
+ }
169
+ when (WireFormat.getTagFieldNumber(tag)) {
170
+ 1 -> relPc = input.readUInt64()
171
+ 2 -> pc = input.readUInt64()
172
+ 4 -> functionName = input.readStringRequireUtf8()
173
+ 5 -> functionOffset = input.readUInt64()
174
+ 6 -> fileName = input.readStringRequireUtf8()
175
+ 8 -> buildId = input.readStringRequireUtf8()
176
+ else -> input.skipField(tag)
177
+ }
178
+ }
179
+ return ParsedBacktraceFrame(
180
+ relPc = relPc,
181
+ pc = pc,
182
+ functionName = functionName,
183
+ functionOffset = functionOffset,
184
+ fileName = fileName,
185
+ buildId = buildId,
186
+ )
187
+ }
188
+
189
+ private fun parseMemoryMapping(bytes: ByteArray): ParsedMemoryMapping {
190
+ val input = CodedInputStream.newInstance(bytes)
191
+ var beginAddress = 0L
192
+ var endAddress = 0L
193
+ var mappingName = ""
194
+ var buildId = ""
195
+ var loadBias = 0L
196
+ while (true) {
197
+ val tag = input.readTag()
198
+ if (tag == 0) {
199
+ break
200
+ }
201
+ when (WireFormat.getTagFieldNumber(tag)) {
202
+ 1 -> beginAddress = input.readUInt64()
203
+ 2 -> endAddress = input.readUInt64()
204
+ 7 -> mappingName = input.readStringRequireUtf8()
205
+ 8 -> buildId = input.readStringRequireUtf8()
206
+ 9 -> loadBias = input.readUInt64()
207
+ else -> input.skipField(tag)
208
+ }
209
+ }
210
+ return ParsedMemoryMapping(
211
+ beginAddress = beginAddress,
212
+ endAddress = endAddress,
213
+ mappingName = mappingName,
214
+ buildId = buildId,
215
+ loadBias = loadBias,
216
+ )
217
+ }
218
+
219
+ private fun extractLengthDelimitedMessages(bytes: ByteArray): List<ByteArray> {
220
+ val nested = mutableListOf<ByteArray>()
221
+ val input = CodedInputStream.newInstance(bytes)
222
+ while (true) {
223
+ val tag = input.readTag()
224
+ if (tag == 0) {
225
+ break
226
+ }
227
+ if (WireFormat.getTagWireType(tag) == WireFormat.WIRETYPE_LENGTH_DELIMITED) {
228
+ nested.add(input.readBytes().toByteArray())
229
+ } else {
230
+ input.skipField(tag)
231
+ }
232
+ }
233
+ return nested
234
+ }
235
+ }
@@ -0,0 +1,62 @@
1
+ // Android tombstone protobuf (AOSP debuggerd/proto/tombstone.proto).
2
+ // Used to decode ApplicationExitInfo.traceInputStream for REASON_CRASH_NATIVE (API 31+).
3
+ // https://developer.android.com/reference/android/app/ApplicationExitInfo#getTraceInputStream()
4
+
5
+ syntax = "proto3";
6
+
7
+ option java_package = "com.grafana.faro.reactnative.tombstone";
8
+ option java_outer_classname = "TombstoneProtos";
9
+
10
+ message Tombstone {
11
+ Architecture arch = 1;
12
+ string build_fingerprint = 2;
13
+ string timestamp = 4;
14
+ uint32 pid = 5;
15
+ uint32 tid = 6;
16
+ Signal signal_info = 10;
17
+ string abort_message = 14;
18
+ map<uint32, Thread> threads = 16;
19
+ repeated MemoryMapping memory_mappings = 17;
20
+ }
21
+
22
+ enum Architecture {
23
+ ARM32 = 0;
24
+ ARM64 = 1;
25
+ X86 = 2;
26
+ X86_64 = 3;
27
+ RISCV64 = 4;
28
+ NONE = 5;
29
+ }
30
+
31
+ message Signal {
32
+ int32 number = 1;
33
+ string name = 2;
34
+ int32 code = 3;
35
+ string code_name = 4;
36
+ }
37
+
38
+ message Thread {
39
+ int32 id = 1;
40
+ string name = 2;
41
+ repeated BacktraceFrame current_backtrace = 4;
42
+ }
43
+
44
+ message BacktraceFrame {
45
+ uint64 rel_pc = 1;
46
+ uint64 pc = 2;
47
+ uint64 sp = 3;
48
+ string function_name = 4;
49
+ uint64 function_offset = 5;
50
+ string file_name = 6;
51
+ uint64 file_map_offset = 7;
52
+ string build_id = 8;
53
+ }
54
+
55
+ message MemoryMapping {
56
+ uint64 begin_address = 1;
57
+ uint64 end_address = 2;
58
+ uint64 offset = 3;
59
+ string mapping_name = 7;
60
+ string build_id = 8;
61
+ uint64 load_bias = 9;
62
+ }
@@ -0,0 +1,28 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class AnrStackFiltersTest {
8
+
9
+ @Test
10
+ fun isCrashHandlerStack_detectsFrameworkCrashKillerFrames() {
11
+ val stack = """
12
+ at com.android.internal.os.RuntimeInit${'$'}KillApplicationHandler.uncaughtException(RuntimeInit.java:174)
13
+ at java.lang.Thread.dispatchUncaughtException(Thread.java:2306)
14
+ """.trimIndent()
15
+
16
+ assertTrue(AnrStackFilters.isCrashHandlerStack(stack))
17
+ }
18
+
19
+ @Test
20
+ fun isCrashHandlerStack_allowsBlockedAppFrames() {
21
+ val stack = """
22
+ at com.example.MainActivity.block(MainActivity.kt:42)
23
+ at android.os.Handler.dispatchMessage(Handler.java:102)
24
+ """.trimIndent()
25
+
26
+ assertFalse(AnrStackFilters.isCrashHandlerStack(stack))
27
+ }
28
+ }
@@ -0,0 +1,38 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class AnrThreadDumpParserTest {
8
+
9
+ @Test
10
+ fun extractMainThreadStack_parsesDalvikThreadDump() {
11
+ val dump = """
12
+ "main" prio=5 tid=1 Runnable
13
+ | group="main" sCount=0 ucsCount=0 flags=0 obj=0x0 self=0x0
14
+ at com.example.MainActivity.block(MainActivity.kt:42)
15
+ at android.app.Activity.performCreate(Activity.java:9000)
16
+ "ReferenceQueueDaemon" prio=5 tid=6 Waiting
17
+ at java.lang.Object.wait(Native method)
18
+ """.trimIndent()
19
+
20
+ val stack = AnrThreadDumpParser.extractMainThreadStack(dump)
21
+
22
+ assertTrue(stack.contains("com.example.MainActivity.block"))
23
+ assertFalse(stack.contains("ReferenceQueueDaemon"))
24
+ }
25
+
26
+ @Test
27
+ fun extractMainThreadStack_parsesFlatAtLines() {
28
+ val dump = """
29
+ at com.example.MainActivity.block(MainActivity.kt:42)
30
+ at android.app.Activity.performCreate(Activity.java:9000)
31
+ """.trimIndent()
32
+
33
+ val stack = AnrThreadDumpParser.extractMainThreadStack(dump)
34
+
35
+ assertTrue(stack.contains("com.example.MainActivity.block(MainActivity.kt:42)"))
36
+ assertTrue(stack.contains("android.app.Activity.performCreate(Activity.java:9000)"))
37
+ }
38
+ }
@@ -0,0 +1,81 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.content.Context
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Before
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+ import org.robolectric.annotation.Config
13
+
14
+ @RunWith(RobolectricTestRunner::class)
15
+ @Config(manifest = Config.NONE, sdk = [30])
16
+ class FaroAnrCacheTest {
17
+
18
+ private lateinit var context: Context
19
+
20
+ @Before
21
+ fun setUp() {
22
+ context = ApplicationProvider.getApplicationContext()
23
+ context.getSharedPreferences("com.grafana.faro.anr_cache", Context.MODE_PRIVATE)
24
+ .edit()
25
+ .clear()
26
+ .commit()
27
+ }
28
+
29
+ @Test
30
+ fun savePendingAnr_dedupesExactTimestamp() {
31
+ val payload = """{"type":"ANR","timestamp":1000,"stacktrace":"at foo"}"""
32
+
33
+ FaroAnrCache.savePendingAnr(context, payload)
34
+ FaroAnrCache.savePendingAnr(context, payload)
35
+
36
+ assertEquals(1, FaroAnrCache.getPendingAnrs(context).size)
37
+ }
38
+
39
+ @Test
40
+ fun savePendingAnr_collapsesNearbyTimestamps() {
41
+ FaroAnrCache.savePendingAnr(
42
+ context,
43
+ """{"type":"ANR","timestamp":1000,"stacktrace":"at foo"}""",
44
+ )
45
+ FaroAnrCache.savePendingAnr(
46
+ context,
47
+ """{"type":"ANR","timestamp":6000,"stacktrace":"at bar"}""",
48
+ )
49
+
50
+ assertEquals(1, FaroAnrCache.getPendingAnrs(context).size)
51
+ }
52
+
53
+ @Test
54
+ fun hasNearbyAnrDetection_matchesWithinWindow() {
55
+ FaroAnrCache.savePendingAnr(
56
+ context,
57
+ """{"type":"ANR","timestamp":1000,"stacktrace":"at foo"}""",
58
+ )
59
+
60
+ assertTrue(FaroAnrCache.hasNearbyAnrDetection(context, 5000))
61
+ assertFalse(FaroAnrCache.hasNearbyAnrDetection(context, 20000))
62
+ }
63
+
64
+ @Test
65
+ fun acknowledgeAnrs_removesMatchingEntries() {
66
+ FaroAnrCache.savePendingAnr(
67
+ context,
68
+ """{"type":"ANR","timestamp":1000,"stacktrace":"at foo"}""",
69
+ )
70
+ FaroAnrCache.savePendingAnr(
71
+ context,
72
+ """{"type":"ANR","timestamp":20000,"stacktrace":"at bar"}""",
73
+ )
74
+
75
+ FaroAnrCache.acknowledgeAnrs(context, listOf(1000L))
76
+
77
+ val remaining = FaroAnrCache.getPendingAnrs(context)
78
+ assertEquals(1, remaining.size)
79
+ assertTrue(remaining[0].contains("\"timestamp\":20000"))
80
+ }
81
+ }