Track decryption failures

This commit is contained in:
Valere 2022-01-05 10:24:08 +01:00
parent 5efe1f4bd8
commit e5431d9fb4
5 changed files with 164 additions and 0 deletions

1
changelog.d/4719.feature Normal file
View file

@ -0,0 +1 @@
Analytics: Track Errors

View file

@ -133,6 +133,11 @@ internal class MXMegolmDecryption(private val userId: String,
if (requestKeysOnFail) {
requestKeysForEvent(event, false)
}
throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
throwable.olmException.message ?: "",
throwable.olmException.message)
}
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)

View file

@ -0,0 +1,149 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics
import im.vector.app.core.flow.tickerFlow
import im.vector.app.features.analytics.plan.Error
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import javax.inject.Inject
import javax.inject.Singleton
data class DecryptionFailure(
val timeStamp: Long = System.currentTimeMillis(),
val roomId: String,
val failedEventId: String,
val error: MXCryptoError.ErrorType
)
private const val GRACE_PERIOD_MILLIS = 20_000
/**
* Tracks decryption errors that are visible to the user.
* When an error is reported it is not directly tracked via analytics, there is a grace period
* that gives the app a few seconds to get the key to decrypt.
*/
@Singleton
class DecryptionFailureTracker @Inject constructor(
private val vectorAnalytics: VectorAnalytics
) {
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
private val failures = mutableListOf<DecryptionFailure>()
private val alreadyReported = mutableListOf<String>()
init {
start()
}
fun start() {
tickerFlow(scope, 5_000)
.onEach {
checkFailures()
}.launchIn(scope)
}
fun stop() {
scope.cancel()
}
fun e2eEventDisplayedInTimeline(roomId: String, eventId: String, error: MXCryptoError.ErrorType?) {
scope.launch(Dispatchers.Default) {
if (error != null) {
addDecryptionFailure(DecryptionFailure(roomId = roomId, failedEventId = eventId, error = error))
} else {
removeFailureForEventId(eventId)
}
}
}
/**
* Can be called when the timeline is disposed in order
* to grace those events as they are not anymore displayed on screen
* */
fun onTimeLineDisposed(roomId: String) {
scope.launch(Dispatchers.Default) {
synchronized(failures) {
failures.removeIf { it.roomId == roomId }
}
}
}
private fun addDecryptionFailure(failure: DecryptionFailure) {
// de duplicate
synchronized(failures) {
if (failures.indexOfFirst { it.failedEventId == failure.failedEventId } == -1) {
failures.add(failure)
}
}
}
private fun removeFailureForEventId(eventId: String) {
synchronized(failures) {
failures.removeIf { it.failedEventId == eventId }
}
}
private fun checkFailures() {
val now = System.currentTimeMillis()
val aggregatedErrors: Map<Error.Name, List<String>>
synchronized(failures) {
val toReport = mutableListOf<DecryptionFailure>()
failures.removeAll { failure ->
(now - failure.timeStamp > GRACE_PERIOD_MILLIS).also {
if (it) {
toReport.add(failure)
}
}
}
aggregatedErrors = toReport
.groupBy { it.error.toAnalyticsErrorName() }
.mapValues {
it.value.map { it.failedEventId }
}
}
aggregatedErrors.forEach { aggregation ->
// there is now way to send the total/sum in posthog, so iterating
aggregation.value
// for now we ignore events already reported even if displayed again?
.filter { alreadyReported.contains(it).not() }
.forEach { failedEventId ->
vectorAnalytics.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key))
alreadyReported.add(failedEventId)
}
}
}
private fun MXCryptoError.ErrorType.toAnalyticsErrorName(): Error.Name {
return when (this) {
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID -> Error.Name.OlmKeysNotSentError
MXCryptoError.ErrorType.OLM -> {
Error.Name.OlmUnspecifiedError
}
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX -> Error.Name.OlmIndexError
else -> Error.Name.UnknownError
}
}
}

View file

@ -38,6 +38,7 @@ import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.call.conference.ConferenceEvent
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
import im.vector.app.features.call.conference.JitsiService
@ -112,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val decryptionFailureTracker: DecryptionFailureTracker,
timelineFactory: TimelineFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
@ -1083,6 +1085,7 @@ class RoomDetailViewModel @AssistedInject constructor(
override fun onCleared() {
timeline.dispose()
timeline.removeAllListeners()
decryptionFailureTracker.onTimeLineDisposed(room.roomId)
if (vectorPreferences.sendTypingNotifs()) {
room.userStopsTyping()
}

View file

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -34,6 +35,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val widgetItemFactory: WidgetItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory,
private val callItemFactory: CallItemFactory,
private val decryptionFailureTracker: DecryptionFailureTracker,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
/**
@ -122,6 +124,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(params)
}
}.also {
if (it != null && event.isEncrypted()) {
decryptionFailureTracker.e2eEventDisplayedInTimeline(event.roomId, event.eventId, event.root.mCryptoError)
}
}
}
} catch (throwable: Throwable) {