parent
5fbee70b58
commit
931bf5c280
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue