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

feat: Track qr-code analytics #WPB-11679 🍒 (#3565) #3664

Merged
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
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.userAccountProfileLink)
ShareLinkButton(state.userAccountProfileLink, 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)))
},
isUserInCall = viewModelSelf::isUserInCall,
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.NotificationChannelsManager
Expand Down Expand Up @@ -103,7 +105,8 @@ class SelfUserProfileViewModel @Inject constructor(
private val notificationChannelsManager: NotificationChannelsManager,
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 @@ -331,6 +334,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 @@ -50,6 +51,9 @@ class SelfQRCodeViewModelTest {
@MockK
lateinit var selfServerConfig: SelfServerConfigUseCase

@MockK
lateinit var analyticsManager: AnonymousAnalyticsManager

val context = mockk<Context>()

init {
Expand All @@ -66,7 +70,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 @@ -90,6 +91,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 @@ -112,7 +116,8 @@ class SelfUserProfileViewModelArrangement {
notificationChannelsManager = notificationChannelsManager,
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"
}
Loading