From abf313270d4c3420ea27d8a5ffc2aa64e15cfbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Zag=C3=B3rski?= Date: Wed, 30 Oct 2024 17:00:13 +0100 Subject: [PATCH] feat: Track qr-code analytics #WPB-11679 (#3565) --- .../ui/userprofile/qr/SelfQRCodeScreen.kt | 31 +++++++++-- .../ui/userprofile/qr/SelfQRCodeViewModel.kt | 8 +++ .../userprofile/self/SelfUserProfileScreen.kt | 1 + .../self/SelfUserProfileViewModel.kt | 9 +++- .../userprofile/qr/SelfQRCodeViewModelTest.kt | 7 ++- .../SelfUserProfileViewModelArrangement.kt | 7 ++- .../feature/analytics/model/AnalyticsEvent.kt | 51 +++++++++++++++++++ 7 files changed, 106 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt index 326c0226e6c..0e724b5aa11 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.userprofile.qr import android.annotation.SuppressLint import android.graphics.Bitmap import android.net.Uri +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -58,6 +59,7 @@ import com.lightspark.composeqr.DotShape import com.lightspark.composeqr.QrCodeView import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination import com.wire.android.navigation.style.SlideNavigationAnimation @@ -90,6 +92,7 @@ fun SelfQRCodeScreen( SelfQRCodeContent( viewModel.selfQRCodeState, viewModel::shareQRAsset, + viewModel::trackAnalyticsEvent, navigator::navigateBack ) } @@ -98,16 +101,26 @@ fun SelfQRCodeScreen( private fun SelfQRCodeContent( state: SelfQRCodeState, shareQRAssetClick: suspend (Bitmap) -> Uri, + trackAnalyticsEvent: (AnalyticsEvent.QrCode.Modal) -> Unit, onBackClick: () -> Unit = {} ) { val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() val context = LocalContext.current + + BackHandler { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Back) + onBackClick() + } + WireScaffold( topBar = { WireCenterAlignedTopAppBar( title = stringResource(id = R.string.user_profile_qr_code_title), - onNavigationPressed = onBackClick, + onNavigationPressed = { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Back) + onBackClick() + }, elevation = 0.dp ) } @@ -190,9 +203,10 @@ private fun SelfQRCodeContent( color = colorsScheme().secondaryText ) Spacer(modifier = Modifier.weight(1f)) - ShareLinkButton(state.userProfileLink) + ShareLinkButton(state.userProfileLink, trackAnalyticsEvent) VerticalSpace.x8() ShareQRCodeButton { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.ShareQrCode) coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() val qrUri = shareQRAssetClick(bitmap.asAndroidBitmap()) @@ -219,7 +233,10 @@ fun ShareQRCodeButton(shareQRAssetClick: () -> Unit) { } @Composable -private fun ShareLinkButton(selfProfileUrl: String) { +private fun ShareLinkButton( + selfProfileUrl: String, + trackAnalyticsEvent: (AnalyticsEvent.QrCode.Modal) -> Unit +) { val context = LocalContext.current WirePrimaryButton( modifier = @@ -229,7 +246,10 @@ private fun ShareLinkButton(selfProfileUrl: String) { .padding(horizontal = dimensions().spacing16x) .testTag("Share link"), text = stringResource(R.string.user_profile_qr_code_share_link), - onClick = { context.shareLinkToProfile(selfProfileUrl) } + onClick = { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.ShareProfileLink) + context.shareLinkToProfile(selfProfileUrl) + } ) } @@ -245,7 +265,8 @@ fun PreviewSelfQRCodeContent() { handle = "userid", userProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555" ), - { "".toUri() } + { "".toUri() }, + { } ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt index a996443e7dc..a2dc5ce6e11 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt @@ -28,6 +28,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.di.CurrentAccount +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.navArgs import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.getTempWritableAttachmentUri @@ -51,6 +53,7 @@ class SelfQRCodeViewModel @Inject constructor( private val selfServerLinks: SelfServerConfigUseCase, private val kaliumFileSystem: KaliumFileSystem, private val dispatchers: DispatcherProvider, + private val analyticsManager: AnonymousAnalyticsManager ) : ViewModel() { private val selfQrCodeNavArgs: SelfQrCodeNavArgs = savedStateHandle.navArgs() var selfQRCodeState by mutableStateOf(SelfQRCodeState(selfUserId, handle = selfQrCodeNavArgs.handle)) @@ -59,6 +62,7 @@ class SelfQRCodeViewModel @Inject constructor( get() = kaliumFileSystem.rootCachePath init { + trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Displayed) viewModelScope.launch { getServerLinks() } @@ -83,6 +87,10 @@ class SelfQRCodeViewModel @Inject constructor( return job.await() } + fun trackAnalyticsEvent(event: AnalyticsEvent.QrCode.Modal) { + analyticsManager.sendEvent(event) + } + private suspend fun getTempWritableQRUri(tempCachePath: Path): Uri = withContext(dispatchers.io()) { val tempImagePath = "$tempCachePath/$TEMP_SELF_QR_FILENAME".toPath() return@withContext getTempWritableAttachmentUri(context, tempImagePath) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index 5c5cf0d3d2e..04a9391523c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -135,6 +135,7 @@ fun SelfUserProfileScreen( onLegalHoldLearnMoreClick = remember { { legalHoldSubjectDialogState.show(Unit) } }, onOtherAccountClick = { viewModelSelf.switchAccount(it, NavigationSwitchAccountActions(navigator::navigate)) }, onQrCodeClick = { + viewModelSelf.trackQrCodeClick() navigator.navigate(NavigationCommand(SelfQRCodeScreenDestination(viewModelSelf.userProfileState.userName))) }, onCreateAccount = { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index ff8f7d56ea0..6c21fc42393 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -33,6 +33,8 @@ import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountActions import com.wire.android.feature.SwitchAccountParam import com.wire.android.feature.SwitchAccountResult +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.mapper.OtherAccountMapper import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.notification.WireNotificationManager @@ -101,7 +103,8 @@ class SelfUserProfileViewModel @Inject constructor( private val isReadOnlyAccount: IsReadOnlyAccountUseCase, private val notificationManager: WireNotificationManager, private val globalDataStore: GlobalDataStore, - private val qualifiedIdMapper: QualifiedIdMapper + private val qualifiedIdMapper: QualifiedIdMapper, + private val analyticsManager: AnonymousAnalyticsManager ) : ViewModel() { var userProfileState by mutableStateOf(SelfUserProfileState(userId = selfUserId, isAvatarLoading = true)) @@ -338,6 +341,10 @@ class SelfUserProfileViewModel @Inject constructor( userProfileState = userProfileState.copy(errorMessageCode = null) } + fun trackQrCodeClick() { + analyticsManager.sendEvent(AnalyticsEvent.QrCode.Click(!userProfileState.teamName.isNullOrBlank())) + } + sealed class ErrorCodes { object DownloadUserInfoError : ErrorCodes() } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt index d12f396c520..dc924723ade 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider +import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.framework.TestUser import com.wire.android.ui.navArgs @@ -45,6 +46,9 @@ class SelfQRCodeViewModelTest { @MockK lateinit var selfServerConfig: SelfServerConfigUseCase + @MockK + lateinit var analyticsManager: AnonymousAnalyticsManager + val context = mockk() init { @@ -61,7 +65,8 @@ class SelfQRCodeViewModelTest { selfUserId = TestUser.SELF_USER.id, selfServerLinks = selfServerConfig, kaliumFileSystem = fakeKaliumFileSystem, - dispatchers = TestDispatcherProvider() + dispatchers = TestDispatcherProvider(), + analyticsManager = analyticsManager ) val fakeKaliumFileSystem: FakeKaliumFileSystem = FakeKaliumFileSystem() diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt index 6766ebe6f8c..4479bcd837f 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -23,6 +23,7 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore import com.wire.android.di.AuthServerConfigProvider import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.framework.TestTeam import com.wire.android.framework.TestUser import com.wire.android.mapper.OtherAccountMapper @@ -87,6 +88,9 @@ class SelfUserProfileViewModelArrangement { @MockK lateinit var qualifiedIdMapper: QualifiedIdMapper + @MockK + lateinit var analyticsManager: AnonymousAnalyticsManager + private val viewModel by lazy { SelfUserProfileViewModel( selfUserId = TestUser.SELF_USER.id, @@ -108,7 +112,8 @@ class SelfUserProfileViewModelArrangement { isReadOnlyAccount = isReadOnlyAccount, notificationManager = notificationManager, globalDataStore = globalDataStore, - qualifiedIdMapper = qualifiedIdMapper + qualifiedIdMapper = qualifiedIdMapper, + analyticsManager = analyticsManager ) } diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt index 418e9a2f895..cb1e4ecbcdd 100644 --- a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt @@ -26,6 +26,8 @@ import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_SCORE_KEY import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CONTRIBUTED_LOCATION import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MESSAGE_ACTION_KEY +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE_TEAM interface AnalyticsEvent { /** @@ -183,6 +185,42 @@ interface AnalyticsEvent { override val messageAction: String = AnalyticsEventConstants.CONTRIBUTED_AUDIO } } + + sealed class QrCode : AnalyticsEvent { + data class Click(val isTeam: Boolean) : QrCode() { + override val key: String = AnalyticsEventConstants.QR_CODE_CLICK + + override fun toSegmentation(): Map { + val userType = if (isTeam) { + QR_CODE_SEGMENTATION_USER_TYPE_TEAM + } else { + QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL + } + + return mapOf( + AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE to userType + ) + } + } + + sealed class Modal : QrCode() { + data object Displayed : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_MODAL + } + + data object Back : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_MODAL_BACK + } + + data object ShareProfileLink : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_SHARE_PROFILE_LINK + } + + data object ShareQrCode : Modal() { + override val key: String = AnalyticsEventConstants.QR_CODE_SHARE_QR_CODE + } + } + } } object AnalyticsEventConstants { @@ -230,4 +268,17 @@ object AnalyticsEventConstants { const val CONTRIBUTED_VIDEO = "video" const val CONTRIBUTED_AUDIO = "audio" const val CONTRIBUTED_LOCATION = "location" + + /** + * Qr code + */ + const val QR_CODE_CLICK = "ui.QR-click" + const val QR_CODE_MODAL = "ui.share.profile" + const val QR_CODE_MODAL_BACK = "user.back.share-profile" + const val QR_CODE_SHARE_PROFILE_LINK = "user.share-profile" + const val QR_CODE_SHARE_QR_CODE = "user.QR-code" + + const val QR_CODE_SEGMENTATION_USER_TYPE = "user_type" + const val QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL = "personal" + const val QR_CODE_SEGMENTATION_USER_TYPE_TEAM = "team" }