Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinned messages analytics #3523

Merged
merged 4 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/kotlin/io/element/android/x/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -64,6 +65,7 @@
CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this),
LocalAnalyticsService provides appBindings.analyticsService(),

Check warning on line 68 in app/src/main/kotlin/io/element/android/x/MainActivity.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/io/element/android/x/MainActivity.kt#L68

Added line #L68 was not covered by tests
) {
Box(
modifier = Modifier
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/io/element/android/x/di/AppBindings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.analytics.api.AnalyticsService

@ContributesTo(AppScope::class)
interface AppBindings {
Expand All @@ -32,4 +33,6 @@ interface AppBindings {
fun migrationEntryPoint(): MigrationEntryPoint

fun lockScreenEntryPoint(): LockScreenEntryPoint

fun analyticsService(): AnalyticsService
}
2 changes: 1 addition & 1 deletion features/messages/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ dependencies {
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.testtags)
implementation(projects.features.networkmonitor.api)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.toolbox.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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.PinUnpinAction
import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents
Expand Down Expand Up @@ -77,6 +78,7 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.room.canCall
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -104,6 +106,7 @@ class MessagesPresenter @AssistedInject constructor(
private val buildMeta: BuildMeta,
private val timelineController: TimelineController,
private val permalinkParser: PermalinkParser,
private val analyticsService: AnalyticsService,
) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
Expand Down Expand Up @@ -285,6 +288,12 @@ class MessagesPresenter @AssistedInject constructor(

private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.Timeline,
kind = PinUnpinAction.Kind.Pin,
)
)
timelineController.invokeOnCurrentTimeline {
pinEvent(targetEvent.eventId)
.onFailure {
Expand All @@ -296,6 +305,12 @@ class MessagesPresenter @AssistedInject constructor(

private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.Timeline,
kind = PinUnpinAction.Kind.Unpin,
)
)
timelineController.invokeOnCurrentTimeline {
unpinEvent(targetEvent.eventId)
.onFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
Expand All @@ -51,6 +52,8 @@ import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndica
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction

@Composable
fun PinnedMessagesBannerView(
Expand Down Expand Up @@ -79,6 +82,7 @@ private fun PinnedMessagesBannerRow(
onViewAllClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val analyticsService = LocalAnalyticsService.current
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
Row(
modifier = modifier
Expand All @@ -88,6 +92,7 @@ private fun PinnedMessagesBannerRow(
.heightIn(min = 64.dp)
.clickable {
if (state is PinnedMessagesBannerState.Loaded) {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerClick)
onClick(state.currentPinnedMessage.eventId)
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
}
Expand All @@ -112,7 +117,13 @@ private fun PinnedMessagesBannerRow(
message = state.formattedMessage(),
modifier = Modifier.weight(1f)
)
ViewAllButton(state, onViewAllClick)
ViewAllButton(
state = state,
onViewAllClick = {
onViewAllClick()
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerViewAllButton)
},
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
Expand All @@ -39,6 +41,8 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
Expand All @@ -58,6 +62,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private val snackbarDispatcher: SnackbarDispatcher,
actionListPresenterFactory: ActionListPresenter.Factory,
private val appCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
) : Presenter<PinnedMessagesListState> {
@AssistedFactory
interface Factory {
Expand Down Expand Up @@ -128,6 +133,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
TimelineItemAction.Unpin -> handleUnpinAction(targetEvent)
TimelineItemAction.ViewInTimeline -> {
targetEvent.eventId?.let { eventId ->
analyticsService.captureInteraction(Interaction.Name.PinnedMessageListViewTimeline)
navigator.onViewInTimelineClick(eventId)
}
}
Expand All @@ -137,6 +143,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(

private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
if (targetEvent.eventId == null) return
analyticsService.capture(
PinUnpinAction(
from = PinUnpinAction.From.MessagePinningList,
kind = PinUnpinAction.Kind.Unpin,
)
)
timelineProvider.invokeOnTimeline {
unpinEvent(targetEvent.eventId)
.onFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView
Expand All @@ -44,6 +45,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction

@Composable
fun PinnedMessagesListView(
Expand All @@ -57,7 +60,14 @@ fun PinnedMessagesListView(
Scaffold(
modifier = modifier,
topBar = {
PinnedMessagesListTopBar(state, onBackClick)
val analyticsService = LocalAnalyticsService.current
PinnedMessagesListTopBar(
state = state,
onBackClick = {
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerCloseListButton)
onBackClick()
}
)
},
content = { padding ->
PinnedMessagesListContent(
Expand All @@ -67,8 +77,8 @@ fun PinnedMessagesListView(
onLinkClick = onLinkClick,
onErrorDismiss = onBackClick,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
.padding(padding)
.consumeWindowInsets(padding),
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
Expand Down Expand Up @@ -896,6 +897,7 @@ class MessagesPresenterTest {
fun `present - handle action pin`() = runTest {
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
val analyticsService = FakeAnalyticsService()
val timeline = FakeTimeline()
val room = FakeMatrixRoom(
liveTimeline = timeline,
Expand All @@ -906,7 +908,7 @@ class MessagesPresenterTest {
typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
Expand All @@ -923,6 +925,10 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline),
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline)
)
}
}

Expand All @@ -931,6 +937,7 @@ class MessagesPresenterTest {
val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
val timeline = FakeTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
Expand All @@ -940,7 +947,7 @@ class MessagesPresenterTest {
typingNoticeResult = { Result.success(Unit) },
canUserPinUnpinResult = { Result.success(true) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
Expand All @@ -957,6 +964,10 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline),
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline)
)
}
}

Expand Down Expand Up @@ -1074,6 +1085,7 @@ class MessagesPresenterTest {
htmlConverterProvider = FakeHtmlConverterProvider(),
timelineController = TimelineController(matrixRoom),
permalinkParser = permalinkParser,
analyticsService = analyticsService,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.pinned.list

import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
Expand All @@ -30,6 +31,8 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
Expand Down Expand Up @@ -142,7 +145,7 @@ class PinnedMessagesListPresenterTest {
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure<Boolean>(A_THROWABLE) }
val pinnedEventsTimeline = createPinnedMessagesTimeline()

val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
Expand All @@ -151,7 +154,7 @@ class PinnedMessagesListPresenterTest {
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true, analyticsService = analyticsService)
presenter.test {
skipItems(3)
val filledState = awaitItem() as PinnedMessagesListState.Filled
Expand All @@ -174,6 +177,11 @@ class PinnedMessagesListPresenterTest {
assert(failureUnpinEventLambda)
.isCalledOnce()
.with(value(AN_EVENT_ID))

assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList),
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList)
)
}
}

Expand Down Expand Up @@ -286,6 +294,7 @@ class PinnedMessagesListPresenterTest {
room: MatrixRoom = FakeMatrixRoom(),
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
isFeatureEnabled: Boolean = true,
analyticsService: AnalyticsService = FakeAnalyticsService(),
): PinnedMessagesListPresenter {
val timelineProvider = PinnedEventsTimelineProvider(
room = room,
Expand All @@ -302,6 +311,7 @@ class PinnedMessagesListPresenterTest {
timelineProvider = timelineProvider,
snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory,
analyticsService = analyticsService,
appCoroutineScope = this,
)
}
Expand Down
2 changes: 1 addition & 1 deletion features/roomdetails/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ dependencies {
implementation(projects.features.createroom.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.userprofile.shared)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.features.poll.api)
implementation(projects.features.messages.api)

Expand Down
Loading
Loading