From 6fc4450c5651f742ea65ae1bc051d69a7496fe4f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Jun 2024 17:54:31 +0200 Subject: [PATCH] Analytics: track screen `MobileScreen.ScreenName.RoomCall` --- features/call/build.gradle.kts | 2 + .../features/call/ui/CallScreenPresenter.kt | 12 +++++ .../call/ui/CallScreenPresenterTest.kt | 49 ++++++++++++++----- gradle/libs.versions.toml | 2 +- services/analytics/test/build.gradle.kts | 3 +- .../analytics/test/FakeScreenTracker.kt | 31 ++++++++++++ 6 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt diff --git a/features/call/build.gradle.kts b/features/call/build.gradle.kts index cd6e235f6a..7ff1510cef 100644 --- a/features/call/build.gradle.kts +++ b/features/call/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.matrix.impl) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) + implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) implementation(libs.serialization.json) @@ -56,5 +57,6 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt index 1ec75ed47d..49f6352212 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.features.call.CallType import io.element.android.features.call.data.WidgetMessage import io.element.android.features.call.utils.CallWidgetProvider @@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect @@ -61,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor( private val clock: SystemClock, private val dispatchers: CoroutineDispatchers, private val matrixClientsProvider: MatrixClientProvider, + private val screenTracker: ScreenTracker, private val appCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory @@ -83,6 +86,15 @@ class CallScreenPresenter @AssistedInject constructor( loadUrl(callType, urlState, callWidgetDriver) } + when (callType) { + is CallType.ExternalUrl -> { + // No analytics yet for external calls + } + is CallType.RoomCall -> { + screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) + } + } + HandleMatrixClientSyncState() callWidgetDriver.value?.let { driver -> diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index a9ddb1bf6b..30579ad99c 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.features.call.CallType import io.element.android.features.call.utils.FakeCallWidgetProvider import io.element.android.features.call.utils.FakeWidgetMessageInterceptor @@ -32,9 +33,13 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.services.analytics.api.ScreenTracker +import io.element.android.services.analytics.test.FakeScreenTracker import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilTimeout +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin @@ -53,16 +58,20 @@ class CallScreenPresenterTest { @Test fun `present - with CallType ExternalUrl just loads the URL`() = runTest { - val presenter = createCallScreenPresenter(CallType.ExternalUrl("https://call.element.io")) + val analyticsLambda = lambdaRecorder { } + val presenter = createCallScreenPresenter( + callType = CallType.ExternalUrl("https://call.element.io"), + screenTracker = FakeScreenTracker(analyticsLambda) + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { // Wait until the URL is loaded skipItems(1) - val initialState = awaitItem() assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) assertThat(initialState.isInWidgetMode).isFalse() + analyticsLambda.assertions().isNeverCalled() } } @@ -70,22 +79,29 @@ class CallScreenPresenterTest { fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest { val widgetDriver = FakeMatrixWidgetDriver() val widgetProvider = FakeCallWidgetProvider(widgetDriver) + val analyticsLambda = lambdaRecorder { } val presenter = createCallScreenPresenter( callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), widgetDriver = widgetDriver, widgetProvider = widgetProvider, + screenTracker = FakeScreenTracker(analyticsLambda) ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { // Wait until the URL is loaded skipItems(1) - val initialState = awaitItem() assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.isInWidgetMode).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetDriver.runCalledCount).isEqualTo(1) + // Called several times because of the recomposition + analyticsLambda.assertions().isCalledExactly(2) + .withSequence( + listOf(value(MobileScreen.ScreenName.RoomCall)), + listOf(value(MobileScreen.ScreenName.RoomCall)) + ) } } @@ -95,6 +111,7 @@ class CallScreenPresenterTest { val presenter = createCallScreenPresenter( callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), widgetDriver = widgetDriver, + screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() moleculeFlow(RecompositionMode.Immediate) { @@ -125,6 +142,7 @@ class CallScreenPresenterTest { widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() moleculeFlow(RecompositionMode.Immediate) { @@ -155,6 +173,7 @@ class CallScreenPresenterTest { widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() moleculeFlow(RecompositionMode.Immediate) { @@ -185,7 +204,8 @@ class CallScreenPresenterTest { widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), - matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }), + screenTracker = FakeScreenTracker {}, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -208,7 +228,8 @@ class CallScreenPresenterTest { widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), - matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }), + screenTracker = FakeScreenTracker {}, ) val hasRun = Mutex(true) val job = launch { @@ -233,6 +254,7 @@ class CallScreenPresenterTest { widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), + screenTracker: ScreenTracker = FakeScreenTracker(), ): CallScreenPresenter { val userAgentProvider = object : UserAgentProvider { override fun provide(): String { @@ -241,14 +263,15 @@ class CallScreenPresenterTest { } val clock = SystemClock { 0 } return CallScreenPresenter( - callType, - navigator, - widgetProvider, - userAgentProvider, - clock, - dispatchers, - matrixClientsProvider, - this, + callType = callType, + navigator = navigator, + callWidgetProvider = widgetProvider, + userAgentProvider = userAgentProvider, + clock = clock, + dispatchers = dispatchers, + matrixClientsProvider = matrixClientsProvider, + screenTracker = screenTracker, + appCoroutineScope = this, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43f369288d..dff3505584 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -187,7 +187,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0" posthog = "com.posthog:posthog-android:3.3.0" sentry = "io.sentry:sentry-android:7.9.0" # main branch can be tested replacing the version with main-SNAPSHOT -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.0" +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" diff --git a/services/analytics/test/build.gradle.kts b/services/analytics/test/build.gradle.kts index 521af86a61..2e0773809d 100644 --- a/services/analytics/test/build.gradle.kts +++ b/services/analytics/test/build.gradle.kts @@ -14,7 +14,7 @@ * limitations under the License. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { @@ -24,5 +24,6 @@ android { dependencies { implementation(projects.services.analytics.api) implementation(projects.libraries.core) + implementation(projects.tests.testutils) implementation(libs.coroutines.core) } diff --git a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt new file mode 100644 index 0000000000..127644aa3c --- /dev/null +++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt @@ -0,0 +1,31 @@ +/* + * 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.services.analytics.test + +import androidx.compose.runtime.Composable +import im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.services.analytics.api.ScreenTracker +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeScreenTracker( + private val trackScreenLambda: (MobileScreen.ScreenName) -> Unit = { lambdaError() } +) : ScreenTracker { + @Composable + override fun TrackScreen(screen: MobileScreen.ScreenName) { + trackScreenLambda(screen) + } +}