TP-81333: Add HP sections impression event instrumentation (#12642)

This commit is contained in:
Hitesh Kumar
2024-09-24 16:35:16 +05:30
committed by GitHub
parent 5d09383085
commit dcd167866c
12 changed files with 144 additions and 4 deletions

View File

@@ -0,0 +1,84 @@
/*
*
* * Copyright © 2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.home.common.handler
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import com.navi.base.utils.orFalse
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.HOME_SCREEN_IMPRESSION_THRESHOLD
import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.model.data.UiTronActionData
import javax.inject.Inject
class HomePageSectionImpressionTracker @Inject constructor() {
private var bottomOverlayBounds: Rect = Rect.Zero
private val sectionImpressionStatesMap = mutableMapOf<String, Boolean>()
private var impressionThreshold: Float = FLOAT_ZERO
companion object {
private const val FLOAT_ZERO = 0F
}
init {
impressionThreshold =
FirebaseRemoteConfigHelper.getDouble(HOME_SCREEN_IMPRESSION_THRESHOLD).toFloat()
}
fun updateSectionImpressionState(
widget: AlchemistWidgetModelDefinition<UiTronResponse>,
coordinates: LayoutCoordinates,
onImpression: (UiTronActionData?) -> Unit
) {
val widgetId = widget.widgetId.orEmpty()
if (isImpressionRecorded(widgetId)) return
if (isSectionHeightAboveThreshold(coordinates)) {
onImpression(widget.widgetRenderActions?.onImpressionAction)
setImpressionState(widgetId, true)
}
}
private fun isSectionHeightAboveThreshold(coordinates: LayoutCoordinates): Boolean {
val sectionBounds = coordinates.boundsInWindow()
val heightUnderBottomOverlay = calculateHeightUnderOverlay(sectionBounds)
val maxSectionHeight = coordinates.size.height
val visibleSectionHeight = sectionBounds.height
return visibleSectionHeight - heightUnderBottomOverlay >=
impressionThreshold * maxSectionHeight
}
private fun calculateHeightUnderOverlay(sectionBounds: Rect): Float {
return if (bottomOverlayBounds == Rect.Zero) FLOAT_ZERO
else maxOf(FLOAT_ZERO, sectionBounds.bottom - bottomOverlayBounds.top)
}
private fun setImpressionState(widgetId: String, isVisible: Boolean) {
sectionImpressionStatesMap[widgetId] = isVisible
}
private fun isImpressionRecorded(widgetId: String): Boolean {
return sectionImpressionStatesMap[widgetId].orFalse()
}
fun updateBottomOverlayBounds(newRect: Rect) {
bottomOverlayBounds = newRect
}
fun resetImpressionStates() {
sectionImpressionStatesMap.keys.forEach { widgetId -> setImpressionState(widgetId, false) }
}
fun getItemImpressionThreshold(): Float {
return impressionThreshold
}
}

View File

@@ -36,6 +36,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
@@ -47,10 +48,12 @@ import com.airbnb.lottie.compose.rememberLottieComposition
import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition
import com.navi.common.alchemist.model.WidgetRenderState
import com.navi.naviwidgets.R
import com.navi.pay.utils.conditional
import com.navi.uitron.model.UiTronResponse
import com.naviapp.home.reducer.HpEffects
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.utils.getHomeWidgetAnimationSpec
import com.naviapp.home.viewmodel.HomeViewModel
@Composable
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@@ -60,6 +63,7 @@ fun FrontLayerContent(
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
renderingFirstTime: Boolean,
homeScrollState: () -> ScrollState,
homeVM: () -> HomeViewModel,
onEvent: (event: HpEvents) -> Unit,
onEffect: (effect: HpEffects) -> Unit
) {
@@ -77,6 +81,7 @@ fun FrontLayerContent(
renderingFirstTime = renderingFirstTime,
elementList = widgets,
homeWidgetRenderer = homeWidgetRenderer,
homeVM = homeVM,
onEvent = onEvent,
onEffect = onEffect
)
@@ -106,10 +111,25 @@ private fun ContentLoaderLottie(renderingFirstTime: Boolean) {
@Composable
private fun AnimatedContainerForWidgets(
isVisible: MutableState<Boolean>,
widget: AlchemistWidgetModelDefinition<UiTronResponse>,
homeVM: HomeViewModel,
homeWidget: @Composable () -> Unit
) {
val setImpressionTracker =
widget.widgetRenderActions?.onImpressionAction?.actions.isNullOrEmpty().not()
AnimatedVisibility(
visible = isVisible.value,
modifier =
Modifier.conditional(setImpressionTracker) {
onGloballyPositioned { layoutCoordinates ->
homeVM.sectionVisibilityTracker.updateSectionImpressionState(
widget,
layoutCoordinates
) { onImpressionAction ->
homeVM.handleActions(onImpressionAction)
}
}
},
enter =
expandVertically(
expandFrom = Alignment.Top,
@@ -136,6 +156,7 @@ private fun RenderUiTronContent(
renderingFirstTime: Boolean,
elementList: List<AlchemistWidgetModelDefinition<UiTronResponse>>,
homeWidgetRenderer: @Composable (UiTronResponse?) -> Unit,
homeVM: () -> HomeViewModel,
onEvent: (event: HpEvents) -> Unit,
onEffect: (effect: HpEffects) -> Unit
) {
@@ -166,7 +187,9 @@ private fun RenderUiTronContent(
}
}
AnimatedContainerForWidgets(visible) { homeWidgetRenderer(element.widgetData) }
AnimatedContainerForWidgets(visible, element, homeVM()) {
homeWidgetRenderer(element.widgetData)
}
}
}
}

View File

@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import com.navi.uitron.model.UiTronResponse
@@ -23,8 +24,10 @@ import com.naviapp.dashboard.viewmodels.DashboardSharedVM
import com.naviapp.home.compose.home.ui.footer.utils.HomeFooterEvents
import com.naviapp.home.compose.home.ui.footer.utils.HomeFooterStates
import com.naviapp.home.compose.home.ui.footer.utils.handleHomeFooterEvent
import com.naviapp.home.compose.home.utils.updateBottomOverlayBounds
import com.naviapp.home.model.BottomBarTabType
import com.naviapp.home.reducer.HpEvents
import com.naviapp.home.viewmodel.HomeViewModel
import com.naviapp.home.viewmodel.SharedVM
import com.naviapp.screenOverlay.nudge.model.NudgeEffect
import com.naviapp.screenOverlay.nudge.model.NudgeEvent
@@ -39,6 +42,7 @@ fun HomeFooterRoot(
dashboardSharedVM: DashboardSharedVM,
bottomNavBarVM: BottomNavBarVM,
sharedVM: SharedVM,
homeViewModel: () -> HomeViewModel,
navController: NavHostController,
selectedTabId: String,
nudgeState: () -> NudgeState,
@@ -52,7 +56,10 @@ fun HomeFooterRoot(
val isHomeTabSelected = remember(selectedTabId) { selectedTabId == BottomBarTabType.HOME.name }
HomeFooter(
modifier = modifier,
modifier =
modifier.onGloballyPositioned { layoutCoordinates ->
updateBottomOverlayBounds(homeViewModel(), layoutCoordinates)
},
selectedTabId = selectedTabId,
bottomNudgeData = bottomStickyNudgeData,
isHomeTabSelected = isHomeTabSelected,

View File

@@ -131,6 +131,7 @@ fun HomeContentFrame(
bottomNavBarVM = bottomNavBarVM,
dashboardSharedVM = dashboardSharedVM,
sharedVM = sharedVM,
homeViewModel = homeVM,
navController = navController,
selectedTabId = selectedTabId,
onHomeEvent = onHomeScreenEvent,

View File

@@ -95,6 +95,7 @@ fun HomeScreenScaffoldRoot(
widgets = (hpStates().screenDefinition)?.screenStructure?.content?.widgets,
frontLayerShape = frontLayerShape,
homeScrollState = homeScrollState,
homeVM = homeVM,
homeWidgetRenderer = homeWidgetRenderer,
onEffect = { homeVM().setEffect { it } },
onEvent = { homeVM().sendEvent(it) },

View File

@@ -12,6 +12,8 @@ import androidx.compose.material.BackdropScaffoldState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
@@ -67,3 +69,13 @@ private fun normalizeOffset(offset: Float, minOffset: Float, maxOffset: Float):
else -> roundedOffset
}
}
fun updateBottomOverlayBounds(homeVM: HomeViewModel, layoutCoordinates: LayoutCoordinates) {
homeVM.sectionVisibilityTracker.updateBottomOverlayBounds(layoutCoordinates.boundsInWindow())
homeVM.viewImpressionTracker.updateBottomOverlayBounds(layoutCoordinates.boundsInWindow())
}
fun resetImpressionStates(homeVM: HomeViewModel) {
homeVM.sectionVisibilityTracker.resetImpressionStates()
homeVM.viewImpressionTracker.resetImpressionStates()
}

View File

@@ -38,6 +38,8 @@ fun InitLifecycleListener(homeVM: () -> HomeViewModel, activity: HomePageActivit
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> {
val threshold = homeVM().sectionVisibilityTracker.getItemImpressionThreshold()
homeVM().viewImpressionTracker.setViewImpressionThreshold(threshold)
AlfredManager.setCurrentScreenName(screenName = activity.screenName)
homeVM()
.observeUPIVpa(
@@ -50,6 +52,7 @@ fun InitLifecycleListener(homeVM: () -> HomeViewModel, activity: HomePageActivit
homeVM().setEffect { HpEffects.OnRenderActions }
homeVM().setEffect { HpEffects.FetchHomeApi }
}
resetImpressionStates(homeVM())
homeVM().setHomeApiRefreshFlag(true)
homeVM().handleUpiAdaptations()
AlfredManager.setCurrentScreenName(screenName = activity.screenName)

View File

@@ -34,6 +34,7 @@ import com.navi.uitron.model.data.UiTronActionData
import com.naviapp.BuildConfig
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.common.navigator.NaviDeepLinkNavigator
import com.naviapp.home.common.handler.HomePageSectionImpressionTracker
import com.naviapp.home.compose.activity.HomePageActivity
import com.naviapp.home.compose.listener.HomeScreenCallbackListener
import com.naviapp.home.reducer.HomeReducer
@@ -78,6 +79,7 @@ constructor(
private val upiUseCase: HandleUpiUseCase,
val rotatingViewHelper: Lazy<RotatingViewHelper>,
val videoViewHelper: Lazy<VideoViewHelper>,
val sectionVisibilityTracker: HomePageSectionImpressionTracker,
) :
BaseMviViewModel<HpStates, HpEvents, HpEffects>(
initialState = HpStates(),

View File

@@ -101,7 +101,7 @@ navi-adverse = "1.3.0-20240917.145607-3"
navi-alfred = "1.15.1"
navi-guarddog = "3.5.0"
navi-pulse = "1.7.0"
navi-uitron = "1.23.0"
navi-uitron = "1.23.0-20240924.072418-2"
navigation = "2.5.3"
okhttp-bom = "4.12.0"
otaliastudios-cameraview = "2.7.2"

View File

@@ -62,7 +62,9 @@ data class AlchemistRenderActions(
null, // These actions will trigger only upon screen rendering after a successful API call,
// not when rendering occurs by consuming data from db
val actionsCorrespondingToKey: List<AlchemistActionsCorrespondingToKey?>? =
null // This contains a list of actions corresponding to specific app events
null, // This contains a list of actions corresponding to specific app events
val onImpressionAction: UiTronActionData? =
null, // These actions will trigger on actual view of the widget on the screen
)
data class AlchemistActionsCorrespondingToKey(

View File

@@ -22,6 +22,7 @@ object FirebaseRemoteConfigHelper {
private const val CONFIG_SYNC_INTERVAL: Long = 60 * 60
const val FRONT_LAYER_UPPER_CONTENT_LENGTH = "FRONT_LAYER_UPPER_CONTENT_LENGTH"
const val SEND_SCREEN_HASH_IN_HOME_SCREEN_API = "SEND_SCREEN_HASH_IN_HOME_SCREEN_API"
const val HOME_SCREEN_IMPRESSION_THRESHOLD = "HOME_SCREEN_IMPRESSION_THRESHOLD"
const val RAGE_TAP_COUNT = "RAGE_TAP_COUNT"
const val RAGE_TAP_DELAY_TIME = "RAGE_TAP_DELAY_TIME"
const val GST_USERNAME_PASSWORD_MAX_LIMIT = "GST_USERNAME_PASSWORD_MAX_LIMIT"

View File

@@ -602,4 +602,8 @@
<key>NAVI_PAY_SEND_MONEY_FTUE_LIMIT</key>
<value>3</value>
</entry>
<entry>
<key>HOME_SCREEN_IMPRESSION_THRESHOLD</key>
<value>1.0</value>
</entry>
</defaultsMap>