diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt index a94df878eb..85f22ce2df 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt @@ -1,22 +1,32 @@ package com.tangem.core.analytics.models /** - * Marker interface for analytics events that should be sent only once per session. + * Marker interface for analytics events that can be throttled by session and/or time. * - * Events implementing this interface will be tracked by their [oneTimeEventId] to ensure - * they are not sent multiple times during the same application session. Once an event - * with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event - * with the same ID will be ignored. + * Events implementing this interface will be tracked by their [oneTimeEventId]. + * + * Behavior depends on [throttleSeconds]: + * - **null** (default): One time per session — event is sent only once during the application session. + * - **non-null**: Time-based throttling — event is not sent if less than [throttleSeconds] seconds + * have passed since the last send for this [oneTimeEventId]. * * @see Analytics.send */ interface OneTimePerSessionEvent { /** - * Unique identifier for the one-time event. + * Unique identifier for the throttled event. * - * This ID is used to track whether the event has already been sent in the current session. - * Events with the same [oneTimeEventId] will only be sent once, even if they are - * different instances of the same event class. + * This ID is used to track whether and when the event was last sent. + * Events with the same [oneTimeEventId] share the same throttling state. */ val oneTimeEventId: String + + /** + * Minimum interval in seconds between sends for this event. + * + * - **null**: One time per session only. Event is sent at most once per session. + * - **non-null**: Don't send if less than this many seconds have passed since the last send. + */ + val throttleSeconds: Long? + get() = null } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index c5630c4906..d6571cb78c 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit /** [REDACTED_AUTHOR] @@ -32,7 +33,7 @@ object Analytics : GlobalAnalyticsEventHandler { private val handlers = mutableMapOf() private val paramsInterceptors = ConcurrentHashMap() - private val oneEventsPerSession = ConcurrentHashMap() + private val throttledEventsState = ConcurrentHashMap() private val analyticsFilters = mutableSetOf() private val analyticsMutex = Mutex() @@ -87,9 +88,7 @@ object Analytics : GlobalAnalyticsEventHandler { override fun send(event: AnalyticsEvent) { analyticsScope.launch { - if (event is OneTimePerSessionEvent && - oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null - ) { + if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) { return@launch } event.params = applyParamsInterceptors(event) @@ -137,6 +136,21 @@ object Analytics : GlobalAnalyticsEventHandler { return interceptedParams } + private fun shouldSendThrottledEvent(event: OneTimePerSessionEvent): Boolean { + val now = System.currentTimeMillis() + val id = event.oneTimeEventId + return when (val throttleMs = event.throttleSeconds?.let(TimeUnit.SECONDS::toMillis)) { + null -> throttledEventsState.putIfAbsent(id, now) == null + else -> throttledEventsState.compute(id) { _, lastSendTime -> + when { + lastSendTime == null -> now + now - lastSendTime >= throttleMs -> now + else -> lastSendTime + } + } == now + } + } + private fun createScope(): CoroutineScope { val name = "Analytics" val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt index 5752717896..63cc26391a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt @@ -108,9 +108,9 @@ internal class NewsDetailsModel @Inject constructor( private val stateFactory by lazy(LazyThreadSafetyMode.NONE) { NewsDetailsStateFactory( currentStateProvider = Provider { _state.value }, - shareManager = shareManager, onStateUpdate = { newState -> _state.update { newState } }, onRetryClick = ::onRetryClicked, + onShareClick = ::onShareClick, ) } @@ -149,6 +149,11 @@ internal class NewsDetailsModel @Inject constructor( urlOpener.openUrl(relatedArticle.url) } + private fun onShareClick(article: ArticleUM) { + shareManager.shareText(article.newsUrl) + analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsShareButtonClick(article.id)) + } + private fun onArticleIndexChanged(newIndex: Int) { stateFactory.updateSelectedArticleIndex(newIndex) val currentArticle = when (state.value.articlesStateUM) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt index 6e43d74467..a3e9a20b25 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt @@ -70,4 +70,20 @@ internal sealed class NewsDetailsAnalyticsEvent( ERROR_MESSAGE to message, ), ) + + data class NewsShareButtonClick( + private val newsId: Int, + ) : NewsDetailsAnalyticsEvent( + event = "News Share Button Clicked", + params = mapOf( + "News Id" to newsId.toString(), + ), + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + override val throttleSeconds: Long = THROTTLE_SECONDS + } + + private companion object { + const val THROTTLE_SECONDS = 10L + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt index d52db614b1..43adcb7ab7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt @@ -1,6 +1,5 @@ package com.tangem.features.feed.model.news.details.factory -import com.tangem.core.navigation.share.ShareManager import com.tangem.features.feed.ui.news.details.state.ArticleUM import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM @@ -10,7 +9,7 @@ import kotlinx.collections.immutable.toImmutableList internal class NewsDetailsStateFactory( private val currentStateProvider: Provider, - private val shareManager: ShareManager, + private val onShareClick: (ArticleUM) -> Unit, private val onStateUpdate: (NewsDetailsUM) -> Unit, private val onRetryClick: () -> Unit, ) { @@ -23,11 +22,7 @@ internal class NewsDetailsStateFactory( articles = articles.toImmutableList(), articlesStateUM = ArticlesStateUM.Content, selectedArticleIndex = selectedIndex, - onShareClick = { - currentArticle?.let { - shareManager.shareText(it.newsUrl) - } - }, + onShareClick = { currentArticle?.let(onShareClick) }, ), ) } @@ -38,11 +33,7 @@ internal class NewsDetailsStateFactory( onStateUpdate( currentState.copy( selectedArticleIndex = newIndex, - onShareClick = { - currentArticle?.let { - shareManager.shareText(it.newsUrl) - } - }, + onShareClick = { currentArticle?.let(onShareClick) }, ), ) }