New: Improved internal metrics for troubleshooting issues.

We now take metrics on how long devices are taking to create transactions so we can begin to understand which devices are in having a bad user experience and later know precisely how much our performance improvements have helped. We also now track submission response errors to help us pinpoint when and why transactions are failing. This is very useful as the canopy grace period expires and transactions begin to not appear for some testers.
This commit is contained in:
Kevin Gorham 2020-12-19 10:31:29 -05:00
parent b45ced18ba
commit 3b3da801da
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
3 changed files with 70 additions and 29 deletions

View File

@ -40,11 +40,16 @@ class FeedbackBugsnag : FeedbackCoordinator.FeedbackObserver {
action.rewindHeight,
action.toString()
)
is Report.Funnel.Send.Error -> SendException(
action.errorCode,
action.errorMessage
)
else -> null
}?.let { exception ->
val details = kotlin.runCatching { action.toMap() }.getOrElse { mapOf() }
// fix: always add details so that we can differentiate a lack of details from a change in the way details should be added
val details = kotlin.runCatching { action.toMap() }.getOrElse { mapOf("hasDetails" to false) }
Bugsnag.notify(exception) { event ->
if (details.isNotEmpty()) event.addMetadata("errorDetails", details)
event.addMetadata("errorDetails", details)
true
}
}
@ -53,4 +58,7 @@ class FeedbackBugsnag : FeedbackCoordinator.FeedbackObserver {
private class ReorgException(errorHeight: Int, rewindHeight: Int, reorgMesssage: String) :
Throwable(reorgMesssage)
private class SendException(errorCode: Int?, errorMessage: String?): RuntimeException(
"Non-fatal error while sending transaction. code: $errorCode message: $errorMessage"
)
}

View File

@ -14,18 +14,19 @@ object Report {
object SendSelected : Send("sendselected", 50)
object SpendingKeyFound : Send("keyfound", 60)
object Creating : Send("creating", 70)
object Cancelled : Send("cancelled", 72)
class Created(id: Long) : Send("created", 80, "id" to id)
object Submitted : Send("submitted", 90)
class Mined(minedHeight: Int) : Send("mined", 100, "minedHeight" to minedHeight)
// Errors
abstract class Error(stepName: String, step: Int, vararg properties: Pair<String, Any>) : Send("error.$stepName", step, "isError" to true, *properties)
object ErrorNotFound : Error("notfound", 51)
class ErrorEncoding(errorCode: Int? = null, errorMessage: String? = null) : Error("encode", 71,
abstract class Error(stepName: String, step: Int, val errorCode: Int?, val errorMessage: String?, vararg properties: Pair<String, Any>) : Send("error.$stepName", step, "isError" to true, *properties)
object ErrorNotFound : Error("notfound", 51, null, "Key not found")
class ErrorEncoding(errorCode: Int? = null, errorMessage: String? = null) : Error("encode", 71, errorCode, errorMessage,
"errorCode" to (errorCode ?: -1),
"errorMessage" to (errorMessage ?: "None")
)
class ErrorSubmitting(errorCode: Int? = null, errorMessage: String? = null) : Error("submit", 81,
class ErrorSubmitting(errorCode: Int? = null, errorMessage: String? = null) : Error("submit", 81, errorCode, errorMessage,
"errorCode" to (errorCode ?: -1),
"errorMessage" to (errorMessage ?: "None")
)
@ -64,6 +65,14 @@ object Report {
val rewindHeight: Int by propertyMap
}
class TxUpdateFailed(t: Throwable) : Feedback.AppError("txupdate", t, false)
abstract class TxError(action: String, val errorCode: Int?, val errorMessage: String?) : Feedback.AppError(
"tx.$action",
"Failed to $action transaction due to $errorMessage",
false,
"errorCode" to (errorCode ?: 1)
)
class TxEncodeError(errorCode: Int?, errorMessage: String?) : TxError("encode", errorCode, errorMessage)
class TxSubmitError(errorCode: Int?, errorMessage: String?) : TxError("submit", errorCode, errorMessage)
}
}
@ -74,7 +83,7 @@ object Report {
*properties
) {
override val key = "issue.$name"
override fun toString() = "occurrence of ${key.replace('.', ' ')}"
override fun toString() = "occurrence of ${key.replace('.', ' ')}${toMap().let { if(it.size > 1) " with ${it.entries}" else "" }}"
// Issues with sending worth monitoring
object SelfSend : Issue("self.send")
@ -83,6 +92,9 @@ object Report {
object MinimumAmount : Issue("minimum.amount")
class TruncatedMemo(memoSize: Int) : Issue("truncated.memo", "memoSize" to memoSize)
class LargeMemo(memoSize: Int) : Issue("large.memo", "memoSize" to memoSize)
class MissingViewkey(recovered: Boolean, needle: String, haystack: String, hasKey: Boolean) : Issue(
"missing.viewkey", "wasAbleToRecover" to recovered, "needle" to needle, "haystack" to haystack, "hasKey" to hasKey
)
}
enum class Screen(val id: String? = null) : Feedback.Action {

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Feedback.Keyed
import cash.z.ecc.android.feedback.Feedback.TimeMetric
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Funnel.Send.SendSelected
import cash.z.ecc.android.feedback.Report.Funnel.Send.SpendingKeyFound
import cash.z.ecc.android.feedback.Report.Issue
@ -17,14 +18,11 @@ import cash.z.ecc.android.feedback.Report.MetricType
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.annotation.OpenForTesting
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.validate.AddressType
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
@ -69,7 +67,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
lockBox.getBytes(Const.Backup.SEED)!!
)
funnel(SpendingKeyFound)
reportIssues(memoToSend)
reportUserInputIssues(memoToSend)
return synchronizer.sendToAddress(
keys[0],
zatoshiAmount,
@ -77,6 +75,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
memoToSend.chunked(ZcashSdk.MAX_MEMO_SIZE).firstOrNull() ?: ""
).onEach {
twig("Received pending txUpdate: ${it?.toString()}")
updateMetrics(it)
reportFailures(it)
}
}
@ -88,21 +88,6 @@ class SendViewModel @Inject constructor() : ViewModel() {
fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX_STANDARD\n$fromAddress" else memo
private fun reportIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when {
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> feedback.report(Issue.TinyAmount)
zatoshiAmount < 100 -> feedback.report(Issue.MicroAmount)
zatoshiAmount == 1L -> feedback.report(Issue.MinimumAmount)
}
memoToSend.length.also {
when {
it > ZcashSdk.MAX_MEMO_SIZE -> feedback.report(Issue.TruncatedMemo(it))
it > (ZcashSdk.MAX_MEMO_SIZE * 0.96) -> feedback.report(Issue.LargeMemo(it))
}
}
}
suspend fun validateAddress(address: String): AddressType =
synchronizer.validateAddress(address)
@ -149,7 +134,43 @@ class SendViewModel @Inject constructor() : ViewModel() {
includeFromAddress = false
}
fun updateMetrics(tx: PendingTransaction) {
//
// Analytics
//
private fun reportFailures(tx: PendingTransaction) {
when {
tx.isCancelled() -> funnel(Send.Cancelled)
tx.isFailedEncoding() -> {
// report that the funnel leaked and also capture a non-fatal app error
funnel(Send.ErrorEncoding(tx.errorCode, tx.errorMessage))
feedback.report(Report.Error.NonFatal.TxEncodeError(tx.errorCode, tx.errorMessage))
}
tx.isFailedSubmit() -> {
// report that the funnel leaked and also capture a non-fatal app error
funnel(Send.ErrorSubmitting(tx.errorCode, tx.errorMessage))
feedback.report(Report.Error.NonFatal.TxSubmitError(tx.errorCode, tx.errorMessage))
}
}
}
private fun reportUserInputIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when {
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> feedback.report(Issue.TinyAmount)
zatoshiAmount < 100 -> feedback.report(Issue.MicroAmount)
zatoshiAmount == 1L -> feedback.report(Issue.MinimumAmount)
}
memoToSend.length.also {
when {
it > ZcashSdk.MAX_MEMO_SIZE -> feedback.report(Issue.TruncatedMemo(it))
it > (ZcashSdk.MAX_MEMO_SIZE * 0.96) -> feedback.report(Issue.LargeMemo(it))
}
}
}
private fun updateMetrics(tx: PendingTransaction) {
try {
when {
tx.isMined() -> TRANSACTION_SUBMITTED to TRANSACTION_MINED by tx.id
@ -165,7 +186,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
}
fun report(metricId: String?) {
private fun report(metricId: String?) {
metrics[metricId]?.let { metric ->
metric.takeUnless { (it.elapsedTime ?: 0) <= 0L }?.let {
viewModelScope.launch {
@ -189,7 +210,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
}
fun funnel(step: Report.Funnel.Send?) {
fun funnel(step: Send?) {
step ?: return
feedback.report(step)
}