From dab5e0d0cada4bb25877ce0d487ef2b102ecbfea Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 2 Nov 2023 09:32:22 +0000 Subject: [PATCH] Add analytics for voice messages (#1706) --- .../impl/send/SendLocationPresenterTest.kt | 10 ++-- features/messages/impl/build.gradle.kts | 1 + .../composer/VoiceMessageComposerPresenter.kt | 14 ++++++ .../messages/MessagesPresenterTest.kt | 2 + .../VoiceMessageComposerPresenterTest.kt | 49 +++++++++++++++++++ ...tFake.kt => FakeMessageComposerContext.kt} | 2 +- .../impl/create/CreatePollPresenterTest.kt | 6 +-- gradle/libs.versions.toml | 2 +- 8 files changed, 76 insertions(+), 10 deletions(-) rename features/messages/test/src/main/kotlin/io/element/android/features/messages/test/{MessageComposerContextFake.kt => FakeMessageComposerContext.kt} (96%) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index b6f0dc8260..5b06090117 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -28,7 +28,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake import io.element.android.features.location.impl.common.permissions.PermissionsState -import io.element.android.features.messages.test.MessageComposerContextFake +import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -49,7 +49,7 @@ class SendLocationPresenterTest { private val permissionsPresenterFake = PermissionsPresenterFake() private val fakeMatrixRoom = FakeMatrixRoom() private val fakeAnalyticsService = FakeAnalyticsService() - private val messageComposerContextFake = MessageComposerContextFake() + private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeLocationActions = FakeLocationActions() private val fakeBuildMeta = aBuildMeta(applicationName = "app name") private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter( @@ -58,7 +58,7 @@ class SendLocationPresenterTest { }, room = fakeMatrixRoom, analyticsService = fakeAnalyticsService, - messageComposerContext = messageComposerContextFake, + messageComposerContext = fakeMessageComposerContext, locationActions = fakeLocationActions, buildMeta = fakeBuildMeta, ) @@ -379,7 +379,7 @@ class SendLocationPresenterTest { shouldShowRationale = false, ) ) - messageComposerContextFake.apply { + fakeMessageComposerContext.apply { composerMode = MessageComposerMode.Edit( eventId = null, defaultContent = "", transactionId = null ) @@ -425,7 +425,7 @@ class SendLocationPresenterTest { shouldShowRationale = false, ) ) - messageComposerContextFake.apply { + fakeMessageComposerContext.apply { composerMode = MessageComposerMode.Edit( eventId = null, defaultContent = "", transactionId = null ) diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index ca3051351f..809cc9441e 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -75,6 +75,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.features.messages.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) testImplementation(projects.libraries.featureflag.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 7cde5739ba..eeab0dc95c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -27,6 +27,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.Lifecycle +import im.vector.app.features.analytics.plan.Composer +import io.element.android.features.messages.api.MessageComposerContext import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope @@ -56,6 +58,7 @@ class VoiceMessageComposerPresenter @Inject constructor( private val analyticsService: AnalyticsService, private val mediaSender: MediaSender, private val player: VoiceMessageComposerPlayer, + private val messageComposerContext: MessageComposerContext, permissionsPresenterFactory: PermissionsPresenter.Factory ) : Presenter { private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO) @@ -151,6 +154,7 @@ class VoiceMessageComposerPresenter @Inject constructor( } isSending = true player.pause() + analyticsService.captureComposerEvent() appCoroutineScope.sendMessage( file = finishedState.file, mimeType = finishedState.mimeType, @@ -236,6 +240,16 @@ class VoiceMessageComposerPresenter @Inject constructor( voiceRecorder.deleteRecording() } + + private fun AnalyticsService.captureComposerEvent() = + analyticsService.capture( + Composer( + inThread = messageComposerContext.composerMode.inThread, + isEditing = messageComposerContext.composerMode.isEditing, + isReply = messageComposerContext.composerMode.isReply, + messageType = Composer.MessageType.VoiceMessage, + ) + ) } private fun VoiceRecorderState.finishedWaveform(): ImmutableList = 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 6eb72777c1..640be695e9 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 @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.media.FakeLocalMediaFactory +import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter @@ -641,6 +642,7 @@ class MessagesPresenterTest { analyticsService, mediaSender, player = VoiceMessageComposerPlayer(FakeMediaPlayer()), + messageComposerContext = FakeMessageComposerContext(), permissionsPresenterFactory, ) val timelinePresenter = TimelinePresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index b35f5c66b3..9788797392 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -25,11 +25,16 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState +import io.element.android.features.messages.test.FakeMessageComposerContext +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaSender @@ -38,6 +43,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.PressEvent import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState @@ -65,6 +71,7 @@ class VoiceMessageComposerPresenterTest { private val matrixRoom = FakeMatrixRoom() private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom) + private val messageComposerContext = FakeMessageComposerContext() companion object { private val RECORDING_DURATION = 1.seconds @@ -275,6 +282,35 @@ class VoiceMessageComposerPresenterTest { } } + @Test + fun `present - sending is tracked`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Send a normal voice message + messageComposerContext.composerMode = MessageComposerMode.Normal + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + skipItems(1) // Sending state + + // Now reply with a voice message + messageComposerContext.composerMode = aReplyMode() + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + val finalState = awaitItem() // Sending state + + assertThat(analyticsService.capturedEvents).containsExactly( + aVoiceMessageComposerEvent(isReply = false), + aVoiceMessageComposerEvent(isReply = true) + ) + + testPauseAndDestroy(finalState) + } + } + @Test fun `present - send while playing`() = runTest { val presenter = createVoiceMessageComposerPresenter() @@ -565,6 +601,7 @@ class VoiceMessageComposerPresenterTest { analyticsService, mediaSender, player = VoiceMessageComposerPlayer(FakeMediaPlayer()), + messageComposerContext = messageComposerContext, FakePermissionsPresenterFactory(permissionsPresenter), ) } @@ -595,3 +632,15 @@ class VoiceMessageComposerPresenterTest { waveform = waveform.toImmutableList(), ) } + +private fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE) + +private fun aVoiceMessageComposerEvent( + isReply: Boolean = false +) = Composer( + inThread = false, + isEditing = false, + isReply = isReply, + messageType = Composer.MessageType.VoiceMessage, + startsThread = null +) diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/FakeMessageComposerContext.kt similarity index 96% rename from features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt rename to features/messages/test/src/main/kotlin/io/element/android/features/messages/test/FakeMessageComposerContext.kt index 03af64e071..d709f5f868 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/FakeMessageComposerContext.kt @@ -19,6 +19,6 @@ package io.element.android.features.messages.test import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.textcomposer.model.MessageComposerMode -class MessageComposerContextFake( +class FakeMessageComposerContext( override var composerMode: MessageComposerMode = MessageComposerMode.Normal ) : MessageComposerContext 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 8e16084a59..4ea3ff1eca 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 @@ -22,7 +22,7 @@ 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.features.messages.test.FakeMessageComposerContext 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 @@ -41,12 +41,12 @@ class CreatePollPresenterTest { private var navUpInvocationsCount = 0 private val fakeMatrixRoom = FakeMatrixRoom() private val fakeAnalyticsService = FakeAnalyticsService() - private val messageComposerContextFake = MessageComposerContextFake() + private val fakeMessageComposerContext = FakeMessageComposerContext() private val presenter = CreatePollPresenter( room = fakeMatrixRoom, analyticsService = fakeAnalyticsService, - messageComposerContext = messageComposerContextFake, + messageComposerContext = fakeMessageComposerContext, navigateUp = { navUpInvocationsCount++ }, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df9d087659..e66d0b3c09 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -171,7 +171,7 @@ opusencoder = "io.element.android:opusencoder:1.1.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" sentry = "io.sentry:sentry-android:6.32.0" -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f" +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:aa14cbcdf81af2746d20a71779ec751f971e1d7f" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"