Enabled crash reporting.

Addresses #54
This commit is contained in:
Kevin Gorham 2020-01-09 10:53:48 -05:00
parent 5fbee70b58
commit 931bf5c280
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
5 changed files with 107 additions and 45 deletions

View File

@ -7,13 +7,21 @@ import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraXConfig
import cash.z.ecc.android.di.component.AppComponent
import cash.z.ecc.android.di.component.DaggerAppComponent
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Provider
class ZcashWalletApp : Application(), CameraXConfig.Provider {
@Inject
lateinit var feedbackCoordinator: FeedbackCoordinator
var creationTime: Long = 0
private set
@ -26,7 +34,8 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
super.onCreate()
component = DaggerAppComponent.factory().create(this)
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
component.inject(this)
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(feedbackCoordinator, Thread.getDefaultUncaughtExceptionHandler()))
Twig.plant(TroubleshootingTwig())
}
@ -44,11 +53,21 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
lateinit var component: AppComponent
}
class ExceptionReporter(val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
/**
* @param feedbackCoordinator inject a provider so that if a crash happens before configuration
* is complete, we can lazily initialize all the feedback objects at this moment so that we
* don't have to add any time to startup.
*/
class ExceptionReporter(private val coordinator: FeedbackCoordinator, private val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread?, e: Throwable?) {
// trackCrash(e, "Top-level exception wasn't caught by anything else!")
// Analytics.clear()
twig("Uncaught Exception: $e")
coordinator.feedback.report(e)
coordinator.flush()
// can do this if necessary but first verify that we need it
runBlocking {
coordinator.await()
coordinator.feedback.stop()
}
ogHandler.uncaughtException(t, e)
}
}

View File

@ -9,6 +9,8 @@ import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(zcashWalletApp: ZcashWalletApp)
// Subcomponents
fun mainActivitySubcomponent(): MainActivitySubcomponent.Factory
fun synchronizerSubcomponent(): SynchronizerSubcomponent.Factory

View File

@ -3,13 +3,11 @@ package cash.z.ecc.android.di.module
import android.content.ClipboardManager
import android.content.Context
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.component.InitializerSubcomponent
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.wallet.sdk.Initializer
import cash.z.ecc.android.feedback.*
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.multibindings.IntoSet
import javax.inject.Singleton
@Module(subcomponents = [MainActivitySubcomponent::class])
@ -23,4 +21,40 @@ class AppModule {
@Singleton
fun provideClipboard(context: Context) =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
//
// Feedback
//
@Provides
@Singleton
fun provideFeedback(): Feedback = Feedback()
@Provides
@Singleton
fun provideFeedbackCoordinator(
feedback: Feedback,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
//
// Default Feedback Observer Set
//
@Provides
@Singleton
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@Singleton
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@Singleton
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
}

View File

@ -1,44 +1,10 @@
package cash.z.ecc.android.di.module
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.component.InitializerSubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.feedback.*
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
@Module(includes = [ViewModelsActivityModule::class], subcomponents = [SynchronizerSubcomponent::class, InitializerSubcomponent::class])
class MainActivityModule {
@Provides
@ActivityScope
fun provideFeedback(): Feedback = Feedback()
@Provides
@ActivityScope
fun provideFeedbackCoordinator(
feedback: Feedback,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
//
// Default Feedback Observer Set
//
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
}

View File

@ -5,6 +5,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.coroutines.coroutineContext
class Feedback(capacity: Int = 256) {
@ -106,10 +108,11 @@ class Feedback(capacity: Int = 256) {
*
* @param metric the metric to add.
*/
fun report(metric: Metric) {
fun report(metric: Metric): Feedback {
jobs += scope.launch {
_metrics.send(metric)
}
return this
}
/**
@ -117,10 +120,21 @@ class Feedback(capacity: Int = 256) {
*
* @param action the action to add.
*/
fun report(action: Action) {
fun report(action: Action): Feedback {
jobs += scope.launch {
_actions.send(action)
}
return this
}
/**
* Report the given error to everything that is tracking feedback. Converts it to a Crash object
* which is intended for use in property-based analytics.
*
* @param error the uncaught exception that occurred.
*/
fun report(error: Throwable?): Feedback {
return report(Crash(error))
}
/**
@ -147,6 +161,7 @@ class Feedback(capacity: Int = 256) {
throw IllegalStateException("Feedback is still active because ${errors.joinToString(", ")}.")
}
interface Metric : Mappable<String, Any> {
val key: String
val startTime: Long?
@ -193,4 +208,30 @@ class Feedback(capacity: Int = 256) {
return "$description in ${elapsedTime}ms"
}
}
}
data class Crash(val error: Throwable?) : Action {
override val key: String = "crash"
override fun toMap(): Map<String, Any> {
return mutableMapOf<String, Any>(
"message" to (error?.message ?: "None"),
"cause" to (error?.cause?.toString() ?: "None"),
"cause.cause" to (error?.cause?.cause?.toString() ?: "None"),
"cause.cause.cause" to (error?.cause?.cause?.cause?.toString() ?: "None")
).apply { putAll(super.toMap()); putAll(error.stacktraceToMap()) }
}
override fun toString() = "App crashed due to: $error"
}
}
private fun Throwable?.stacktraceToMap(chunkSize: Int = 250): Map<out String, String> {
val properties = mutableMapOf("stacktrace0" to "None")
if (this == null) return properties
val stringWriter = StringWriter()
printStackTrace(PrintWriter(stringWriter))
stringWriter.toString().chunked(chunkSize).forEachIndexed { index, chunk ->
properties["stacktrace$index"] = chunk
}
return properties
}