mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-26 19:36:08 +03:00
Merge pull request #4856 from vector-im/feature/bca/posthog_e2e
Track decryption failures
This commit is contained in:
commit
fd854a6172
5 changed files with 169 additions and 0 deletions
1
changelog.d/4719.feature
Normal file
1
changelog.d/4719.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Analytics: Track Errors
|
|
@ -133,6 +133,11 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
if (requestKeysOnFail) {
|
if (requestKeysOnFail) {
|
||||||
requestKeysForEvent(event, false)
|
requestKeysForEvent(event, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw MXCryptoError.Base(
|
||||||
|
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
||||||
|
"UNKNOWN_MESSAGE_INDEX",
|
||||||
|
null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* 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.core.time.Clock
|
||||||
|
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 org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private data class DecryptionFailure(
|
||||||
|
val timeStamp: Long,
|
||||||
|
val roomId: String,
|
||||||
|
val failedEventId: String,
|
||||||
|
val error: MXCryptoError.ErrorType
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val GRACE_PERIOD_MILLIS = 4_000
|
||||||
|
private const val CHECK_INTERVAL = 2_000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 clock: Clock
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val scope: CoroutineScope = CoroutineScope(SupervisorJob())
|
||||||
|
private val failures = mutableListOf<DecryptionFailure>()
|
||||||
|
private val alreadyReported = mutableListOf<String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
tickerFlow(scope, CHECK_INTERVAL)
|
||||||
|
.onEach {
|
||||||
|
checkFailures()
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun e2eEventDisplayedInTimeline(event: TimelineEvent) {
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
val mCryptoError = event.root.mCryptoError
|
||||||
|
if (mCryptoError != null) {
|
||||||
|
addDecryptionFailure(DecryptionFailure(clock.epochMillis(), event.roomId, event.eventId, mCryptoError))
|
||||||
|
} else {
|
||||||
|
removeFailureForEventId(event.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.none { it.failedEventId == failure.failedEventId }) {
|
||||||
|
failures.add(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeFailureForEventId(eventId: String) {
|
||||||
|
synchronized(failures) {
|
||||||
|
failures.removeIf { it.failedEventId == eventId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkFailures() {
|
||||||
|
val now = clock.epochMillis()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import im.vector.app.core.mvrx.runCatchingToAsync
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.BehaviorDataSource
|
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.ConferenceEvent
|
||||||
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
||||||
import im.vector.app.features.call.conference.JitsiService
|
import im.vector.app.features.call.conference.JitsiService
|
||||||
|
@ -112,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
private val directRoomHelper: DirectRoomHelper,
|
private val directRoomHelper: DirectRoomHelper,
|
||||||
private val jitsiService: JitsiService,
|
private val jitsiService: JitsiService,
|
||||||
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
||||||
|
private val decryptionFailureTracker: DecryptionFailureTracker,
|
||||||
timelineFactory: TimelineFactory
|
timelineFactory: TimelineFactory
|
||||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||||
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
|
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener {
|
||||||
|
@ -1083,6 +1085,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
timeline.removeAllListeners()
|
timeline.removeAllListeners()
|
||||||
|
decryptionFailureTracker.onTimeLineDisposed(room.roomId)
|
||||||
if (vectorPreferences.sendTypingNotifs()) {
|
if (vectorPreferences.sendTypingNotifs()) {
|
||||||
room.userStopsTyping()
|
room.userStopsTyping()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.TimelineEmptyItem_
|
import im.vector.app.core.epoxy.TimelineEmptyItem_
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
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 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.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
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 widgetItemFactory: WidgetItemFactory,
|
||||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||||
private val callItemFactory: CallItemFactory,
|
private val callItemFactory: CallItemFactory,
|
||||||
|
private val decryptionFailureTracker: DecryptionFailureTracker,
|
||||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,6 +124,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
Timber.v("Type ${event.root.getClearType()} not handled")
|
Timber.v("Type ${event.root.getClearType()} not handled")
|
||||||
defaultItemFactory.create(params)
|
defaultItemFactory.create(params)
|
||||||
}
|
}
|
||||||
|
}.also {
|
||||||
|
if (it != null && event.isEncrypted()) {
|
||||||
|
decryptionFailureTracker.e2eEventDisplayedInTimeline(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
|
|
Loading…
Reference in a new issue