From 8abaee870805a5ccf24c9058d114c6403e67009e Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 12 Sep 2023 16:03:40 +0200 Subject: [PATCH] Polls analytics (#1285) Send poll analytics event (creation, vote, end) where appropriate. --- .../messages/impl/MessagesPresenter.kt | 9 +++++-- .../impl/timeline/TimelinePresenter.kt | 5 +++- .../messages/MessagesPresenterTest.kt | 17 +++++++++--- .../timeline/TimelinePresenterTest.kt | 18 ++++++++++--- features/poll/impl/build.gradle.kts | 2 ++ .../poll/impl/create/CreatePollNode.kt | 7 ++--- .../poll/impl/create/CreatePollPresenter.kt | 26 ++++++++++++++++--- .../impl/create/CreatePollPresenterTest.kt | 26 +++++++++++++++++-- 8 files changed, 90 insertions(+), 20 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index b0ff403984..0831afb699 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -30,6 +30,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.PollEnd import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction @@ -74,6 +75,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.room.canRedactAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -92,6 +94,7 @@ class MessagesPresenter @AssistedInject constructor( private val messageSummaryFormatter: MessageSummaryFormatter, private val dispatchers: CoroutineDispatchers, private val clipboardHelper: ClipboardHelper, + private val analyticsService: AnalyticsService, @Assisted private val navigator: MessagesNavigator, ) : Presenter { @@ -321,8 +324,10 @@ class MessagesPresenter @AssistedInject constructor( } private suspend fun handleEndPollAction(event: TimelineItem.Event) { - event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") } - // TODO Polls: Send poll end analytic + event.eventId?.let { + room.endPoll(it, "The poll with event id: $it has ended.") + analyticsService.capture(PollEnd()) + } } private suspend fun handleCopyContents(event: TimelineItem.Event) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index bf0f61e258..a7e86d341f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter @@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.ui.room.canSendMessageAsState +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn @@ -51,6 +53,7 @@ class TimelinePresenter @Inject constructor( private val room: MatrixRoom, private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, + private val analyticsService: AnalyticsService, ) : Presenter { private val timeline = room.timeline @@ -93,7 +96,7 @@ class TimelinePresenter @Inject constructor( pollStartId = event.pollStartId, answers = listOf(event.answerId), ) - // TODO Polls: Send poll vote analytic + analyticsService.capture(PollVote()) } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index ed7c4a8064..2a601fbb90 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -21,6 +21,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.PollEnd import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.InviteDialogAction @@ -575,7 +576,11 @@ class MessagesPresenterTest { @Test fun `present - handle poll end`() = runTest { val room = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom = room) + val analyticsService = FakeAnalyticsService() + val presenter = createMessagePresenter( + matrixRoom = room, + analyticsService = analyticsService, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -586,7 +591,8 @@ class MessagesPresenterTest { assertThat(room.endPollInvocations.size).isEqualTo(1) assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.") - // TODO Polls: Test poll end analytic + assertThat(analyticsService.capturedEvents.size).isEqualTo(1) + assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd()) } } @@ -595,6 +601,7 @@ class MessagesPresenterTest { matrixRoom: MatrixRoom = FakeMatrixRoom(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): MessagesPresenter { val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, @@ -604,7 +611,7 @@ class MessagesPresenterTest { localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom), snackbarDispatcher = SnackbarDispatcher(), - analyticsService = FakeAnalyticsService(), + analyticsService = analyticsService, messageComposerContext = MessageComposerContextImpl(), richTextEditorStateFactory = TestRichTextEditorStateFactory(), @@ -613,7 +620,8 @@ class MessagesPresenterTest { timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, dispatchers = coroutineDispatchers, - appScope = this + appScope = this, + analyticsService = analyticsService, ) val buildMeta = aBuildMeta() val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) @@ -633,6 +641,7 @@ class MessagesPresenterTest { messageSummaryFormatter = FakeMessageSummaryFormatter(), navigator = navigator, clipboardHelper = clipboardHelper, + analyticsService = analyticsService, dispatchers = coroutineDispatchers, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 4e8ab07d95..eb5a7b72e1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.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.PollVote import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter @@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.aMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitWithLatch import io.element.android.tests.testutils.testCoroutineDispatchers @@ -260,7 +262,11 @@ class TimelinePresenterTest { @Test fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest { val room = FakeMatrixRoom() - val presenter = createTimelinePresenter(room) + val analyticsService = FakeAnalyticsService() + val presenter = createTimelinePresenter( + room = room, + analyticsService = analyticsService, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -271,7 +277,8 @@ class TimelinePresenterTest { assertThat(room.sendPollResponseInvocations.size).isEqualTo(1) assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId")) assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) - // TODO Polls: Test poll vote analytic + assertThat(analyticsService.capturedEvents.size).isEqualTo(1) + assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote()) } private fun TestScope.createTimelinePresenter( @@ -282,18 +289,21 @@ class TimelinePresenterTest { timelineItemsFactory = timelineItemsFactory, room = FakeMatrixRoom(matrixTimeline = timeline), dispatchers = testCoroutineDispatchers(), - appScope = this + appScope = this, + analyticsService = FakeAnalyticsService(), ) } private fun TestScope.createTimelinePresenter( room: MatrixRoom, + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = room, dispatchers = testCoroutineDispatchers(), - appScope = this + appScope = this, + analyticsService = analyticsService, ) } } diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts index e2139424e1..5ff9025aae 100644 --- a/features/poll/impl/build.gradle.kts +++ b/features/poll/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.services.analytics.api) + implementation(projects.features.messages.api) implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) @@ -48,6 +49,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.features.messages.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt index 387f57a597..506c39c177 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt @@ -24,15 +24,17 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope +import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) class CreatePollNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: CreatePollPresenter.Factory, - // analyticsService: AnalyticsService, // TODO Polls: add analytics + analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { private val presenter = presenterFactory.create(backNavigator = ::navigateUp) @@ -40,8 +42,7 @@ class CreatePollNode @AssistedInject constructor( init { lifecycle.subscribe( onResume = { - // TODO Polls: add analytics - // analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.PollView)) + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreatePollView)) } ) } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt index b44afae9d1..44cc54a100 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -29,9 +29,13 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.analytics.plan.PollCreation +import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @@ -44,9 +48,9 @@ private const val MAX_SELECTIONS = 1 class CreatePollPresenter @AssistedInject constructor( private val room: MatrixRoom, - // private val analyticsService: AnalyticsService, // TODO Polls: add analytics + private val analyticsService: AnalyticsService, + private val messageComposerContext: MessageComposerContext, @Assisted private val navigateUp: () -> Unit, - // private val messageComposerContext: MessageComposerContext, // TODO Polls: add analytics ) : Presenter { @AssistedFactory @@ -78,7 +82,21 @@ class CreatePollPresenter @AssistedInject constructor( maxSelections = MAX_SELECTIONS, pollKind = pollKind, ) - // analyticsService.capture(PollCreate()) // TODO Polls: add analytics + analyticsService.capture( + Composer( + inThread = messageComposerContext.composerMode.inThread, + isEditing = messageComposerContext.composerMode.isEditing, + isReply = messageComposerContext.composerMode.isReply, + messageType = Composer.MessageType.Poll, + ) + ) + analyticsService.capture( + PollCreation( + action = PollCreation.Action.Create, + isUndisclosed = pollKind == PollKind.Undisclosed, + numberOfAnswers = answers.size, + ) + ) navigateUp() } else { Timber.d("Cannot create poll") @@ -153,7 +171,7 @@ private val pollKindSaver: Saver, Boolean> = Saver( }, restore = { mutableStateOf( - when(it) { + when (it) { true -> PollKind.Undisclosed else -> PollKind.Disclosed } diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index c54972e9d1..a58fb5476b 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -20,9 +20,13 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.analytics.plan.PollCreation +import io.element.android.features.messages.test.MessageComposerContextFake import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.room.CreatePollInvocation import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest @@ -37,11 +41,13 @@ class CreatePollPresenterTest { private var navUpInvocationsCount = 0 private val fakeMatrixRoom = FakeMatrixRoom() - // private val fakeAnalyticsService = FakeAnalyticsService() // TODO Polls: add analytics + private val fakeAnalyticsService = FakeAnalyticsService() + private val messageComposerContextFake = MessageComposerContextFake() private val presenter = CreatePollPresenter( room = fakeMatrixRoom, - // analyticsService = fakeAnalyticsService, // TODO Polls: add analytics + analyticsService = fakeAnalyticsService, + messageComposerContext = messageComposerContextFake, navigateUp = { navUpInvocationsCount++ }, ) @@ -104,6 +110,22 @@ class CreatePollPresenterTest { pollKind = PollKind.Disclosed ) ) + Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2) + Truth.assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo( + Composer( + inThread = false, + isEditing = false, + isReply = false, + messageType = Composer.MessageType.Poll, + ) + ) + Truth.assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo( + PollCreation( + action = PollCreation.Action.Create, + isUndisclosed = false, + numberOfAnswers = 2, + ) + ) } }