Skip to content

Commit

Permalink
feat: Track qr-code analytics #WPB-11679 (#3565)
Browse files Browse the repository at this point in the history
  • Loading branch information
m-zagorski authored Oct 30, 2024
1 parent 41b23b2 commit abf3132
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -90,6 +92,7 @@ fun SelfQRCodeScreen(
SelfQRCodeContent(
viewModel.selfQRCodeState,
viewModel::shareQRAsset,
viewModel::trackAnalyticsEvent,
navigator::navigateBack
)
}
Expand All @@ -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
)
}
Expand Down Expand Up @@ -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())
Expand All @@ -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 =
Expand All @@ -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)
}
)
}

Expand All @@ -245,7 +265,8 @@ fun PreviewSelfQRCodeContent() {
handle = "userid",
userProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555"
),
{ "".toUri() }
{ "".toUri() },
{ }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -59,6 +62,7 @@ class SelfQRCodeViewModel @Inject constructor(
get() = kaliumFileSystem.rootCachePath

init {
trackAnalyticsEvent(AnalyticsEvent.QrCode.Modal.Displayed)
viewModelScope.launch {
getServerLinks()
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,6 +46,9 @@ class SelfQRCodeViewModelTest {
@MockK
lateinit var selfServerConfig: SelfServerConfigUseCase

@MockK
lateinit var analyticsManager: AnonymousAnalyticsManager

val context = mockk<Context>()

init {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -108,7 +112,8 @@ class SelfUserProfileViewModelArrangement {
isReadOnlyAccount = isReadOnlyAccount,
notificationManager = notificationManager,
globalDataStore = globalDataStore,
qualifiedIdMapper = qualifiedIdMapper
qualifiedIdMapper = qualifiedIdMapper,
analyticsManager = analyticsManager
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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<String, Any> {
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 {
Expand Down Expand Up @@ -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"
}

0 comments on commit abf3132

Please sign in to comment.