diff --git a/changelog.d/2544.misc b/changelog.d/2544.misc new file mode 100644 index 0000000000..2d64ba871e --- /dev/null +++ b/changelog.d/2544.misc @@ -0,0 +1 @@ + Track UTD errors. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13a16b4acf..46d946fe19 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -176,7 +176,7 @@ kotlinpoet = "com.squareup:kotlinpoet:1.16.0" # Analytics posthog = "com.posthog:posthog-android:3.1.15" sentry = "io.sentry:sentry-android:7.6.0" -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.11.0" +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.14.0" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 8815214dd6..d50bad6696 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) + implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) implementation(projects.libraries.featureflag.api) api(projects.libraries.matrix.api) @@ -52,6 +53,7 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.analytics.test) testImplementation(libs.coroutines.test) testImplementation(libs.test.turbine) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 64f152cd9c..80302933d5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.matrix.impl.analytics.UtdTracker import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.network.useragent.UserAgentProvider @@ -42,6 +43,7 @@ class RustMatrixClientFactory @Inject constructor( private val userCertificatesProvider: UserCertificatesProvider, private val proxyProvider: ProxyProvider, private val clock: SystemClock, + private val utdTracker: UtdTracker, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { val client = ClientBuilder() @@ -68,6 +70,7 @@ class RustMatrixClientFactory @Inject constructor( client.restoreSession(sessionData.toSession()) val syncService = client.syncService() + .withUtdHook(utdTracker) .finish() RustMatrixClient( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt new file mode 100644 index 0000000000..578782d6c2 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 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 io.element.android.libraries.matrix.impl.analytics + +import im.vector.app.features.analytics.plan.Error +import io.element.android.services.analytics.api.AnalyticsService +import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate +import org.matrix.rustcomponents.sdk.UnableToDecryptInfo +import timber.log.Timber +import javax.inject.Inject + +class UtdTracker @Inject constructor( + private val analyticsService: AnalyticsService, +) : UnableToDecryptDelegate { + override fun onUtd(info: UnableToDecryptInfo) { + Timber.d("onUtd for event ${info.eventId}, timeToDecryptMs: ${info.timeToDecryptMs}") + val event = Error( + context = null, + // Keep cryptoModule for compatibility. + cryptoModule = Error.CryptoModule.Rust, + cryptoSDK = Error.CryptoSDK.Rust, + timeToDecryptMillis = info.timeToDecryptMs?.toInt() ?: -1, + domain = Error.Domain.E2EE, + // TODO get a more specific error name from `info` + name = Error.Name.OlmKeysNotSentError, + ) + analyticsService.capture(event) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt new file mode 100644 index 0000000000..18ccdbb3e8 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 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 io.element.android.libraries.matrix.impl.analytics + +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.Error +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.services.analytics.test.FakeAnalyticsService +import org.junit.Test +import org.matrix.rustcomponents.sdk.UnableToDecryptInfo + +class UtdTrackerTest { + @Test + fun `when onUtd is called with null timeToDecryptMs, the expected analytics Event is sent`() { + val fakeAnalyticsService = FakeAnalyticsService() + val sut = UtdTracker(fakeAnalyticsService) + sut.onUtd(UnableToDecryptInfo(eventId = AN_EVENT_ID.value, timeToDecryptMs = null)) + assertThat(fakeAnalyticsService.capturedEvents).containsExactly( + Error( + context = null, + cryptoModule = Error.CryptoModule.Rust, + cryptoSDK = Error.CryptoSDK.Rust, + timeToDecryptMillis = -1, + domain = Error.Domain.E2EE, + name = Error.Name.OlmKeysNotSentError + ) + ) + assertThat(fakeAnalyticsService.screenEvents).isEmpty() + assertThat(fakeAnalyticsService.trackedErrors).isEmpty() + } + + @Test + fun `when onUtd is called with timeToDecryptMs, the expected analytics Event is sent`() { + val fakeAnalyticsService = FakeAnalyticsService() + val sut = UtdTracker(fakeAnalyticsService) + sut.onUtd(UnableToDecryptInfo(eventId = AN_EVENT_ID.value, timeToDecryptMs = 123.toULong())) + assertThat(fakeAnalyticsService.capturedEvents).containsExactly( + Error( + context = null, + cryptoModule = Error.CryptoModule.Rust, + cryptoSDK = Error.CryptoSDK.Rust, + timeToDecryptMillis = 123, + domain = Error.Domain.E2EE, + name = Error.Name.OlmKeysNotSentError + ) + ) + assertThat(fakeAnalyticsService.screenEvents).isEmpty() + assertThat(fakeAnalyticsService.trackedErrors).isEmpty() + } +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index 1e5d1e7fd9..687a4e1c50 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -29,10 +29,12 @@ import androidx.core.view.WindowCompat import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.impl.RustMatrixClientFactory +import io.element.android.libraries.matrix.impl.analytics.UtdTracker import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService import io.element.android.libraries.network.useragent.SimpleUserAgentProvider import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock import kotlinx.coroutines.runBlocking import java.io.File @@ -59,6 +61,7 @@ class MainActivity : ComponentActivity() { userCertificatesProvider = userCertificatesProvider, proxyProvider = proxyProvider, clock = DefaultSystemClock(), + utdTracker = UtdTracker(NoopAnalyticsService()), ), passphraseGenerator = NullPassphraseGenerator(), buildMeta = Singleton.buildMeta, diff --git a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt index 65b4a78b9a..14ef329ae4 100644 --- a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt +++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt @@ -31,6 +31,7 @@ class FakeAnalyticsService( private val isEnabledFlow = MutableStateFlow(isEnabled) private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) val capturedEvents = mutableListOf() + val screenEvents = mutableListOf() val trackedErrors = mutableListOf() override fun getAvailableAnalyticsProviders(): Set = emptySet() @@ -60,6 +61,7 @@ class FakeAnalyticsService( } override fun screen(screen: VectorAnalyticsScreen) { + screenEvents += screen } override fun updateUserProperties(userProperties: UserProperties) { diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt index 16cac5722d..fb542b7f28 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogAnalyticsProvider.kt @@ -20,6 +20,7 @@ import com.posthog.PostHogInterface import com.squareup.anvil.annotations.ContributesMultibinding import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen +import im.vector.app.features.analytics.plan.Error import im.vector.app.features.analytics.plan.UserProperties import io.element.android.libraries.di.AppScope import io.element.android.services.analyticsproviders.api.AnalyticsProvider @@ -56,11 +57,17 @@ class PosthogAnalyticsProvider @Inject constructor( } override fun capture(event: VectorAnalyticsEvent) { - posthog?.capture(event.getName(), properties = event.getProperties()?.keepOnlyNonNullValues()) + posthog?.capture( + event = event.getName(), + properties = event.getProperties()?.keepOnlyNonNullValues().withExtraProperties(), + ) } override fun screen(screen: VectorAnalyticsScreen) { - posthog?.screen(screen.getName(), properties = screen.getProperties()) + posthog?.screen( + screenTitle = screen.getName(), + properties = screen.getProperties().withExtraProperties(), + ) } override fun updateUserProperties(userProperties: UserProperties) { @@ -98,3 +105,14 @@ private fun Map.keepOnlyNonNullValues(): Map { } return result } + +/** + * Properties which will be added to all Events and Screens. + */ +private val extraProperties: Map = mapOf( + "cryptoSDK" to Error.CryptoSDK.Rust +) + +private fun Map?.withExtraProperties(): Map { + return orEmpty() + extraProperties +}