@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.
- package/android/build.gradle +118 -36
- package/android/consumer-proguard-rules.pro +2 -0
- package/android/src/main/java/com/grafana/faro/reactnative/ANRTracker.kt +61 -9
- package/android/src/main/java/com/grafana/faro/reactnative/AnrThreadDumpParser.kt +70 -0
- package/android/src/main/java/com/grafana/faro/reactnative/ApplicationExitTraceReader.kt +142 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrCache.kt +163 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrReporter.kt +163 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +243 -50
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +75 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroNativeCrashTrace.kt +39 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +99 -8
- package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativePackage.kt +1 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroUncaughtExceptionHandler.kt +62 -0
- package/android/src/main/java/com/grafana/faro/reactnative/TombstoneBacktraceFormatter.kt +117 -0
- package/android/src/main/java/com/grafana/faro/reactnative/TombstoneModels.kt +35 -0
- package/android/src/main/java/com/grafana/faro/reactnative/TombstoneWireParser.kt +235 -0
- package/android/src/main/proto/tombstone.proto +62 -0
- package/android/src/test/java/com/grafana/faro/reactnative/AnrStackFiltersTest.kt +28 -0
- package/android/src/test/java/com/grafana/faro/reactnative/AnrThreadDumpParserTest.kt +38 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroAnrCacheTest.kt +81 -0
- package/android/src/test/java/com/grafana/faro/reactnative/TombstoneWireParserTest.kt +51 -0
- package/dist/cjs/config/getRNInstrumentations.js +3 -1
- package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
- package/dist/cjs/config/makeRNConfig.js +48 -9
- package/dist/cjs/config/makeRNConfig.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/initialize.js +14 -8
- package/dist/cjs/initialize.js.map +1 -1
- package/dist/cjs/instrumentations/anr/anrStack.js +19 -0
- package/dist/cjs/instrumentations/anr/anrStack.js.map +1 -0
- package/dist/cjs/instrumentations/anr/instrumentation.js +374 -72
- package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/anr/types.js.map +1 -1
- package/dist/cjs/instrumentations/console/index.js +5 -1
- package/dist/cjs/instrumentations/console/index.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +110 -103
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +41 -0
- package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +87 -0
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js +123 -0
- package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/index.js +12 -0
- package/dist/cjs/instrumentations/crashReporting/android/index.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js +11 -0
- package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +181 -0
- package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/index.js +35 -2
- package/dist/cjs/instrumentations/crashReporting/index.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +78 -0
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js +103 -0
- package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/ios/index.js +11 -0
- package/dist/cjs/instrumentations/crashReporting/ios/index.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js +48 -0
- package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/cjs/instrumentations/errors/index.js +2 -1
- package/dist/cjs/instrumentations/errors/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/installationId.js +91 -0
- package/dist/cjs/instrumentations/session/installationId.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionAttributes.js +152 -89
- package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/cjs/metas/appBuildIdentity.js +95 -0
- package/dist/cjs/metas/appBuildIdentity.js.map +1 -0
- package/dist/cjs/testUtils/mockDeviceInfo.js +5 -0
- package/dist/cjs/testUtils/mockDeviceInfo.js.map +1 -1
- package/dist/cjs/testUtils/setupTests.js +7 -0
- package/dist/cjs/testUtils/setupTests.js.map +1 -1
- package/dist/esm/config/getRNInstrumentations.js +3 -1
- package/dist/esm/config/getRNInstrumentations.js.map +1 -1
- package/dist/esm/config/makeRNConfig.js +49 -10
- package/dist/esm/config/makeRNConfig.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/initialize.js +11 -5
- package/dist/esm/initialize.js.map +1 -1
- package/dist/esm/instrumentations/anr/anrStack.js +16 -0
- package/dist/esm/instrumentations/anr/anrStack.js.map +1 -0
- package/dist/esm/instrumentations/anr/instrumentation.js +260 -67
- package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/anr/types.js.map +1 -1
- package/dist/esm/instrumentations/console/index.js +5 -1
- package/dist/esm/instrumentations/console/index.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +76 -94
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +18 -0
- package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +56 -0
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js +116 -0
- package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/index.js +4 -0
- package/dist/esm/instrumentations/crashReporting/android/index.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js +8 -0
- package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +174 -0
- package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/index.js +35 -1
- package/dist/esm/instrumentations/crashReporting/index.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +56 -0
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js +98 -0
- package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/ios/index.js +4 -0
- package/dist/esm/instrumentations/crashReporting/ios/index.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js +44 -0
- package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/esm/instrumentations/errors/index.js +2 -1
- package/dist/esm/instrumentations/errors/index.js.map +1 -1
- package/dist/esm/instrumentations/session/installationId.js +48 -0
- package/dist/esm/instrumentations/session/installationId.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionAttributes.js +83 -38
- package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/esm/metas/appBuildIdentity.js +56 -0
- package/dist/esm/metas/appBuildIdentity.js.map +1 -0
- package/dist/esm/testUtils/mockDeviceInfo.js +5 -0
- package/dist/esm/testUtils/mockDeviceInfo.js.map +1 -1
- package/dist/esm/testUtils/setupTests.js +7 -0
- package/dist/esm/testUtils/setupTests.js.map +1 -1
- package/dist/types/config/makeRNConfig.d.ts +4 -3
- package/dist/types/config/types.d.ts +2 -1
- package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/initialize.d.ts +4 -2
- package/dist/types/instrumentations/anr/anrStack.d.ts +7 -0
- package/dist/types/instrumentations/anr/instrumentation.d.ts +23 -28
- package/dist/types/instrumentations/anr/types.d.ts +5 -1
- package/dist/types/instrumentations/appState/index.d.ts +1 -1
- package/dist/types/instrumentations/console/index.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +56 -0
- package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +12 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +26 -0
- package/dist/types/instrumentations/crashReporting/android/crashErrorMessage.d.ts +23 -0
- package/dist/types/instrumentations/crashReporting/android/index.d.ts +4 -0
- package/dist/types/instrumentations/crashReporting/android/nativeTombstoneTrace.d.ts +2 -0
- package/dist/types/instrumentations/crashReporting/android/parseAndroidCrashTrace.d.ts +45 -0
- package/dist/types/instrumentations/crashReporting/index.d.ts +25 -2
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +28 -0
- package/dist/types/instrumentations/crashReporting/ios/crashErrorMessage.d.ts +14 -0
- package/dist/types/instrumentations/crashReporting/ios/index.d.ts +4 -0
- package/dist/types/instrumentations/crashReporting/ios/parseIosCrashTrace.d.ts +38 -0
- package/dist/types/instrumentations/crashReporting/types.d.ts +18 -51
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +1 -1
- package/dist/types/instrumentations/http/index.d.ts +1 -1
- package/dist/types/instrumentations/performance/index.d.ts +1 -1
- package/dist/types/instrumentations/session/index.d.ts +1 -1
- package/dist/types/instrumentations/session/installationId.d.ts +1 -0
- package/dist/types/instrumentations/session/sessionAttributes.d.ts +13 -4
- package/dist/types/instrumentations/startup/index.d.ts +1 -1
- package/dist/types/instrumentations/userActions/index.d.ts +1 -1
- package/dist/types/instrumentations/view/index.d.ts +1 -1
- package/dist/types/instrumentations/xhr/index.d.ts +1 -1
- package/dist/types/metas/appBuildIdentity.d.ts +16 -0
- package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
- package/dist/types/transports/console/transport.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +1 -1
- package/dist/types/transports/offline/transport.d.ts +1 -1
- package/ios/faro-upload-composed-source-map.sh +8 -2
- package/package.json +5 -5
- package/src/config/getRNInstrumentations.ts +5 -1
- package/src/config/makeRNConfig.test.ts +59 -3
- package/src/config/makeRNConfig.ts +48 -9
- package/src/config/types.ts +2 -1
- package/src/generated/faroRNPackageMeta.ts +1 -1
- package/src/index.ts +2 -1
- package/src/initialize.test.ts +71 -15
- package/src/initialize.ts +11 -5
- package/src/instrumentations/anr/anrStack.test.ts +27 -0
- package/src/instrumentations/anr/anrStack.ts +19 -0
- package/src/instrumentations/anr/instrumentation.test.ts +45 -0
- package/src/instrumentations/anr/instrumentation.ts +301 -67
- package/src/instrumentations/anr/types.ts +7 -1
- package/src/instrumentations/console/index.ts +5 -1
- package/src/instrumentations/console/instrumentation.test.ts +5 -0
- package/src/instrumentations/crashReporting/{instrumentation.ts → BaseCrashReportingInstrumentation.ts} +119 -93
- package/src/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.ts +23 -0
- package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +62 -0
- package/src/instrumentations/crashReporting/android/crashErrorMessage.test.ts +182 -0
- package/src/instrumentations/crashReporting/android/crashErrorMessage.ts +135 -0
- package/src/instrumentations/crashReporting/android/index.ts +4 -0
- package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.test.ts +19 -0
- package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.ts +8 -0
- package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.test.ts +232 -0
- package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.ts +235 -0
- package/src/instrumentations/crashReporting/index.ts +43 -2
- package/src/instrumentations/crashReporting/instrumentation.test.ts +42 -0
- package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +62 -0
- package/src/instrumentations/crashReporting/ios/crashErrorMessage.test.ts +170 -0
- package/src/instrumentations/crashReporting/ios/crashErrorMessage.ts +111 -0
- package/src/instrumentations/crashReporting/ios/index.ts +4 -0
- package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.test.ts +86 -0
- package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.ts +83 -0
- package/src/instrumentations/crashReporting/types.ts +19 -58
- package/src/instrumentations/errors/index.ts +2 -1
- package/src/instrumentations/errors/instrumentation.test.ts +8 -2
- package/src/instrumentations/errors/stackTraceParser.test.ts +1 -1
- package/src/instrumentations/session/installationId.ts +43 -0
- package/src/instrumentations/session/sessionAttributes.test.ts +260 -30
- package/src/instrumentations/session/sessionAttributes.ts +112 -22
- package/src/metas/appBuildIdentity.test.ts +48 -0
- package/src/metas/appBuildIdentity.ts +50 -0
- package/src/testUtils/mockDeviceInfo.ts +5 -0
- package/src/testUtils/setupTests.ts +7 -0
- package/dist/cjs/instrumentations/crashReporting/instrumentation.js.map +0 -1
- package/dist/esm/instrumentations/crashReporting/instrumentation.js.map +0 -1
- 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
|
+
}
|