diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt index 871782e473c..dbf1970a466 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt @@ -23,8 +23,12 @@ import im.vector.app.features.analytics.plan.UserProperties interface AnalyticsTracker { /** * Capture an Event. + * + * @param event The event to capture. + * @param extraProperties Some extra properties to attach to the event, that are not part of the events definition + * (https://github.com/matrix-org/matrix-analytics-events/) and specific to this platform. */ - fun capture(event: VectorAnalyticsEvent) + fun capture(event: VectorAnalyticsEvent, extraProperties: Map? = null) /** * Track a displayed screen. diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index d596741d532..5d47ab89c7b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -27,6 +27,7 @@ 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.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @@ -36,7 +37,9 @@ private data class DecryptionFailure( val timeStamp: Long, val roomId: String, val failedEventId: String, - val error: MXCryptoError.ErrorType + val error: MXCryptoError.ErrorType, + // Was the current session cross signed verified at the time of the error + val isCrossSignedVerified: Boolean = false, ) private typealias DetailedErrorName = Pair @@ -75,11 +78,14 @@ class DecryptionFailureTracker @Inject constructor( scope.cancel() } - fun e2eEventDisplayedInTimeline(event: TimelineEvent) { + fun e2eEventDisplayedInTimeline(event: TimelineEvent, session: Session) { scope.launch(Dispatchers.Default) { val mCryptoError = event.root.mCryptoError if (mCryptoError != null) { - addDecryptionFailure(DecryptionFailure(clock.epochMillis(), event.roomId, event.eventId, mCryptoError)) + val isVerified = session.cryptoService().crossSigningService().isCrossSigningVerified() + addDecryptionFailure( + DecryptionFailure(clock.epochMillis(), event.roomId, event.eventId, mCryptoError, isVerified) + ) } else { removeFailureForEventId(event.eventId) } @@ -115,7 +121,7 @@ class DecryptionFailureTracker @Inject constructor( private fun checkFailures() { val now = clock.epochMillis() - val aggregatedErrors: Map> + val aggregatedErrors: Map> synchronized(failures) { val toReport = mutableListOf() failures.removeAll { failure -> @@ -129,7 +135,7 @@ class DecryptionFailureTracker @Inject constructor( aggregatedErrors = toReport .groupBy { it.error.toAnalyticsErrorName() } .mapValues { - it.value.map { it.failedEventId } + it.value } } @@ -137,15 +143,18 @@ class DecryptionFailureTracker @Inject constructor( // 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 -> - analyticsTracker.capture(Error( - context = aggregation.key.first, - domain = Error.Domain.E2EE, - name = aggregation.key.second, - cryptoModule = currentModule - )) - alreadyReported.add(failedEventId) + .filter { alreadyReported.contains(it.failedEventId).not() } + .forEach { failure -> + analyticsTracker.capture( + event = Error( + context = aggregation.key.first, + domain = Error.Domain.E2EE, + name = aggregation.key.second, + cryptoModule = currentModule, + ), + extraProperties = mapOf("is_cross_signed_verified" to failure.isCrossSignedVerified.toString()) + ) + alreadyReported.add(failure.failedEventId) } } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 2a7d0ac975f..01dd8dff334 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -20,6 +20,7 @@ import com.posthog.android.Options import com.posthog.android.PostHog import com.posthog.android.Properties import im.vector.app.core.di.NamedGlobalScope +import im.vector.app.core.resources.BuildMeta import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.itf.VectorAnalyticsEvent @@ -46,7 +47,8 @@ class DefaultVectorAnalytics @Inject constructor( private val analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, - @NamedGlobalScope private val globalScope: CoroutineScope + @NamedGlobalScope private val globalScope: CoroutineScope, + buildMeta: BuildMeta, ) : VectorAnalytics { private var posthog: PostHog? = null @@ -68,6 +70,20 @@ class DefaultVectorAnalytics @Inject constructor( // Cache for the properties to send private var pendingUserProperties: UserProperties? = null + /** + * Super Properties are properties associated with events that are set once and then sent with every capture call. + */ + private val superProperties: Map = mapOf( + // Put the appVersion (e.g 1.6.12). + "appVersion" to buildMeta.versionName, + // The appId (im.vector.app) + "applicationId" to buildMeta.applicationId, + // The app flavor (GooglePlay, FDroid) + "appFlavor" to buildMeta.flavorDescription, + // Parity with other platforms + "cryptoSDK" to "Rust", + ) + override fun init() { observeUserConsent() observeAnalyticsId() @@ -171,18 +187,24 @@ class DefaultVectorAnalytics @Inject constructor( } } - override fun capture(event: VectorAnalyticsEvent) { + override fun capture(event: VectorAnalyticsEvent, extraProperties: Map?) { Timber.tag(analyticsTag.value).d("capture($event)") posthog ?.takeIf { userConsent == true } - ?.capture(event.getName(), event.getProperties()?.toPostHogProperties()) + ?.capture( + event.getName(), + (superProperties + event.getProperties().orEmpty() + extraProperties.orEmpty()).toPostHogProperties() + ) } override fun screen(screen: VectorAnalyticsScreen) { Timber.tag(analyticsTag.value).d("screen($screen)") posthog ?.takeIf { userConsent == true } - ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) + ?.screen( + screen.getName(), + (superProperties + screen.getProperties().orEmpty()).toPostHogProperties() + ) } override fun updateUserProperties(userProperties: UserProperties) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 84b71ceedfc..4fbaf404d99 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -159,7 +159,7 @@ class TimelineItemFactory @Inject constructor( } }.also { if (it != null && event.isEncrypted()) { - decryptionFailureTracker.e2eEventDisplayedInTimeline(event) + decryptionFailureTracker.e2eEventDisplayedInTimeline(event, session) } } }