@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
|
@@ -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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
|