Updated on 2026-08-14
This commit is contained in:
parent
1bb825744e
commit
92e21cc79c
5 changed files with 62 additions and 26 deletions
|
|
@ -1,22 +1,32 @@
|
||||||
package com.tangem.core.analytics.models
|
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
|
* Events implementing this interface will be tracked by their [oneTimeEventId].
|
||||||
* 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
|
* Behavior depends on [throttleSeconds]:
|
||||||
* with the same ID will be ignored.
|
* - **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
|
* @see Analytics.send
|
||||||
*/
|
*/
|
||||||
interface OneTimePerSessionEvent {
|
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.
|
* This ID is used to track whether and when the event was last sent.
|
||||||
* Events with the same [oneTimeEventId] will only be sent once, even if they are
|
* Events with the same [oneTimeEventId] share the same throttling state.
|
||||||
* different instances of the same event class.
|
|
||||||
*/
|
*/
|
||||||
val oneTimeEventId: String
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
|
|
@ -32,7 +33,7 @@ object Analytics : GlobalAnalyticsEventHandler {
|
||||||
|
|
||||||
private val handlers = mutableMapOf<String, AnalyticsHandler>()
|
private val handlers = mutableMapOf<String, AnalyticsHandler>()
|
||||||
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
|
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
|
||||||
private val oneEventsPerSession = ConcurrentHashMap<String, Boolean>()
|
private val throttledEventsState = ConcurrentHashMap<String, Long>()
|
||||||
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
||||||
private val analyticsMutex = Mutex()
|
private val analyticsMutex = Mutex()
|
||||||
|
|
||||||
|
|
@ -87,9 +88,7 @@ object Analytics : GlobalAnalyticsEventHandler {
|
||||||
|
|
||||||
override fun send(event: AnalyticsEvent) {
|
override fun send(event: AnalyticsEvent) {
|
||||||
analyticsScope.launch {
|
analyticsScope.launch {
|
||||||
if (event is OneTimePerSessionEvent &&
|
if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) {
|
||||||
oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null
|
|
||||||
) {
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
event.params = applyParamsInterceptors(event)
|
event.params = applyParamsInterceptors(event)
|
||||||
|
|
@ -137,6 +136,21 @@ object Analytics : GlobalAnalyticsEventHandler {
|
||||||
return interceptedParams
|
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 {
|
private fun createScope(): CoroutineScope {
|
||||||
val name = "Analytics"
|
val name = "Analytics"
|
||||||
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||||
|
|
|
||||||
|
|
@ -108,9 +108,9 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
private val stateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
private val stateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
NewsDetailsStateFactory(
|
NewsDetailsStateFactory(
|
||||||
currentStateProvider = Provider { _state.value },
|
currentStateProvider = Provider { _state.value },
|
||||||
shareManager = shareManager,
|
|
||||||
onStateUpdate = { newState -> _state.update { newState } },
|
onStateUpdate = { newState -> _state.update { newState } },
|
||||||
onRetryClick = ::onRetryClicked,
|
onRetryClick = ::onRetryClicked,
|
||||||
|
onShareClick = ::onShareClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +149,11 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
urlOpener.openUrl(relatedArticle.url)
|
urlOpener.openUrl(relatedArticle.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onShareClick(article: ArticleUM) {
|
||||||
|
shareManager.shareText(article.newsUrl)
|
||||||
|
analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsShareButtonClick(article.id))
|
||||||
|
}
|
||||||
|
|
||||||
private fun onArticleIndexChanged(newIndex: Int) {
|
private fun onArticleIndexChanged(newIndex: Int) {
|
||||||
stateFactory.updateSelectedArticleIndex(newIndex)
|
stateFactory.updateSelectedArticleIndex(newIndex)
|
||||||
val currentArticle = when (state.value.articlesStateUM) {
|
val currentArticle = when (state.value.articlesStateUM) {
|
||||||
|
|
|
||||||
|
|
@ -70,4 +70,20 @@ internal sealed class NewsDetailsAnalyticsEvent(
|
||||||
ERROR_MESSAGE to message,
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.features.feed.model.news.details.factory
|
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.ArticleUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||||
|
|
@ -10,7 +9,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
internal class NewsDetailsStateFactory(
|
internal class NewsDetailsStateFactory(
|
||||||
private val currentStateProvider: Provider<NewsDetailsUM>,
|
private val currentStateProvider: Provider<NewsDetailsUM>,
|
||||||
private val shareManager: ShareManager,
|
private val onShareClick: (ArticleUM) -> Unit,
|
||||||
private val onStateUpdate: (NewsDetailsUM) -> Unit,
|
private val onStateUpdate: (NewsDetailsUM) -> Unit,
|
||||||
private val onRetryClick: () -> Unit,
|
private val onRetryClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -23,11 +22,7 @@ internal class NewsDetailsStateFactory(
|
||||||
articles = articles.toImmutableList(),
|
articles = articles.toImmutableList(),
|
||||||
articlesStateUM = ArticlesStateUM.Content,
|
articlesStateUM = ArticlesStateUM.Content,
|
||||||
selectedArticleIndex = selectedIndex,
|
selectedArticleIndex = selectedIndex,
|
||||||
onShareClick = {
|
onShareClick = { currentArticle?.let(onShareClick) },
|
||||||
currentArticle?.let {
|
|
||||||
shareManager.shareText(it.newsUrl)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -38,11 +33,7 @@ internal class NewsDetailsStateFactory(
|
||||||
onStateUpdate(
|
onStateUpdate(
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
selectedArticleIndex = newIndex,
|
selectedArticleIndex = newIndex,
|
||||||
onShareClick = {
|
onShareClick = { currentArticle?.let(onShareClick) },
|
||||||
currentArticle?.let {
|
|
||||||
shareManager.shareText(it.newsUrl)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue