Merge pull request #2970 from element-hq/feature/bma/moreAnalytics
Track when the user starts a room call and when they enable formatting options on the message composer
This commit is contained in:
1
changelog.d/2969.misc
Normal file
1
changelog.d/2969.misc
Normal file
@@ -0,0 +1 @@
|
||||
Track when the user starts a room call and when they enable formatting options on the message composer
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<CallScreenState> {
|
||||
@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 ->
|
||||
|
||||
@@ -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<MobileScreen.ScreenName, Unit> { }
|
||||
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<MobileScreen.ScreenName, Unit> { }
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.call.CallType
|
||||
import io.element.android.features.call.ui.ElementCallActivity
|
||||
@@ -68,6 +69,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@@ -80,6 +83,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
@@ -188,6 +192,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
sessionId = matrixClient.sessionId,
|
||||
roomId = roomId,
|
||||
)
|
||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||
ElementCallActivity.start(context, inputs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
|
||||
@@ -68,6 +69,7 @@ import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -388,6 +390,9 @@ class MessageComposerPresenter @Inject constructor(
|
||||
is MessageComposerEvents.ToggleTextFormatting -> {
|
||||
showAttachmentSourcePicker = false
|
||||
showTextFormatting = event.enabled
|
||||
if (showTextFormatting) {
|
||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomComposerFormattingEnabled)
|
||||
}
|
||||
}
|
||||
is MessageComposerEvents.Error -> {
|
||||
analyticsService.trackError(event.error)
|
||||
|
||||
@@ -26,6 +26,7 @@ import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
@@ -768,10 +769,15 @@ class MessageComposerPresenterTest {
|
||||
val showTextFormatting = awaitItem()
|
||||
assertThat(showTextFormatting.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(showTextFormatting.showTextFormatting).isTrue()
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
Interaction(index = null, interactionType = null, name = Interaction.Name.MobileRoomComposerFormattingEnabled)
|
||||
)
|
||||
analyticsService.capturedEvents.clear()
|
||||
showTextFormatting.eventSink(MessageComposerEvents.ToggleTextFormatting(false))
|
||||
skipItems(1)
|
||||
val finished = awaitItem()
|
||||
assertThat(finished.showTextFormatting).isFalse()
|
||||
assertThat(analyticsService.capturedEvents).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.call.CallType
|
||||
import io.element.android.features.call.ui.ElementCallActivity
|
||||
@@ -53,6 +54,8 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@@ -62,6 +65,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pollHistoryEntryPoint: PollHistoryEntryPoint,
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||
@@ -142,6 +146,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||
sessionId = room.sessionId,
|
||||
roomId = room.roomId,
|
||||
)
|
||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||
ElementCallActivity.start(context, inputs)
|
||||
}
|
||||
}
|
||||
@@ -189,8 +194,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||
plugins<RoomDetailsEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
|
||||
override fun onStartCall(roomId: RoomId) {
|
||||
ElementCallActivity.start(context, CallType.RoomCall(roomId = roomId, sessionId = room.sessionId))
|
||||
override fun onStartCall(dmRoomId: RoomId) {
|
||||
ElementCallActivity.start(context, CallType.RoomCall(sessionId = room.sessionId, roomId = dmRoomId))
|
||||
}
|
||||
}
|
||||
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
|
||||
|
||||
@@ -83,8 +83,8 @@ class UserProfileFlowNode @AssistedInject constructor(
|
||||
plugins<UserProfileEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||
}
|
||||
|
||||
override fun onStartCall(roomId: RoomId) {
|
||||
ElementCallActivity.start(context, CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = roomId))
|
||||
override fun onStartCall(dmRoomId: RoomId) {
|
||||
ElementCallActivity.start(context, CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId))
|
||||
}
|
||||
}
|
||||
val params = UserProfileNode.UserProfileInputs(userId = inputs<UserProfileEntryPoint.Params>().userId)
|
||||
|
||||
@@ -32,7 +32,7 @@ class UserProfileNodeHelper(
|
||||
interface Callback : NodeInputs {
|
||||
fun openAvatarPreview(username: String, avatarUrl: String)
|
||||
fun onStartDM(roomId: RoomId)
|
||||
fun onStartCall(roomId: RoomId)
|
||||
fun onStartCall(dmRoomId: RoomId)
|
||||
}
|
||||
|
||||
fun onShareUser(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user