Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-20 19:18:58 +02:00
parent 5aa2af25f1
commit 90e81f1868
3 changed files with 133 additions and 60 deletions

View file

@ -64,14 +64,10 @@ internal class DefaultMarketsTokenRepository(
// we shouldn't infinitely retry on the first batch request
val res = if (isFirstBatchFetching) {
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
catchListErrorAndSendEvent { requestCall() }
} else {
retryOnError(priority = true) {
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
catchListErrorAndSendEvent { requestCall() }
}
}
@ -100,7 +96,7 @@ internal class DefaultMarketsTokenRepository(
marketsApi = marketsApi,
analyticsEventHandler = analyticsEventHandler,
onApiError = {
analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent())
analyticsEventHandler.send(createListErrorEvent(it).toEvent())
},
)
@ -128,11 +124,9 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(),
)
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart,
tokenSymbol = tokenSymbol,
),
val result = catchDetailsErrorAndSendEvent(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart,
tokenSymbol = tokenSymbol,
) {
response.getOrThrow()
}
@ -146,6 +140,7 @@ internal class DefaultMarketsTokenRepository(
analyticsEventHandler.send(
MarketsDataAnalyticsEvent.ChartNullValuesError(
requestPath = "coins/history",
errorType = MarketsDataAnalyticsEvent.Type.Custom,
),
)
},
@ -159,18 +154,16 @@ internal class DefaultMarketsTokenRepository(
tokenSymbol: String,
) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinsListCharts(
coinIds = mappedTokenId,
currency = fiatCurrencyCode,
interval = interval.toRequestParam(),
)
val chart = catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
response.getOrThrow()[mappedTokenId] ?: error(
val chart = catchListErrorAndSendEvent {
marketsApi.getCoinsListCharts(
coinIds = mappedTokenId,
currency = fiatCurrencyCode,
interval = interval.toRequestParam(),
).getOrThrow()[mappedTokenId] ?: error(
"No chart preview data for the token $mappedTokenId",
)
}
return@withContext TokenChartConverter.convert(
interval = interval,
value = chart,
@ -180,6 +173,7 @@ internal class DefaultMarketsTokenRepository(
analyticsEventHandler.send(
MarketsDataAnalyticsEvent.ChartNullValuesError(
requestPath = "coins/history_preview",
errorType = MarketsDataAnalyticsEvent.Type.Custom,
),
)
},
@ -192,19 +186,15 @@ internal class DefaultMarketsTokenRepository(
tokenSymbol: String,
languageCode: String,
) = withContext(dispatcherProvider.io) {
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
language = languageCode,
)
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
val result = catchDetailsErrorAndSendEvent(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
) {
response.getOrThrow()
marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
language = languageCode,
).getOrThrow()
}
val resultResponse = result.applyL2Compatibility(tokenId)
@ -214,19 +204,16 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String) =
withContext(dispatcherProvider.io) {
// for second markets iteration we should use extended api method with all required fields
val response = tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode,
coinIds = tokenId,
fields = marketsQuoteFields.joinToString(separator = ","),
)
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
val result = catchDetailsErrorAndSendEvent(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
) {
response.getOrThrow()
tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode,
coinIds = tokenId,
fields = marketsQuoteFields.joinToString(separator = ","),
).getOrThrow()
}
return@withContext TokenQuotesShortConverter.convert(tokenId, result).toFull()
@ -272,15 +259,75 @@ internal class DefaultMarketsTokenRepository(
}
}
private inline fun <T> catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T {
inline fun <T> catchListErrorAndSendEvent(block: () -> T): T {
return catchErrorAndSendEvent(block, ::createListErrorEvent)
}
private inline fun <T> catchDetailsErrorAndSendEvent(
request: MarketsDataAnalyticsEvent.Details.Error.Request,
tokenSymbol: String,
block: () -> T,
): T {
return catchErrorAndSendEvent(block) { error ->
createDetailsErrorEvent(error, request, tokenSymbol)
}
}
private inline fun <T> catchErrorAndSendEvent(
block: () -> T,
createErrorEvent: (ApiResponseError) -> MarketsDataAnalyticsEvent,
): T {
return try {
block()
} catch (e: ApiResponseError.HttpException) {
analyticsEventHandler.send(errorEvent.toEvent())
throw e
} catch (e: ApiResponseError.TimeoutException) {
} catch (e: ApiResponseError) {
val errorEvent = createErrorEvent(e)
analyticsEventHandler.send(errorEvent.toEvent())
throw e
}
}
private fun createListErrorEvent(error: ApiResponseError): MarketsDataAnalyticsEvent.List.Error {
return createErrorEvent(error) { errorType, errorCode ->
MarketsDataAnalyticsEvent.List.Error(
errorType = errorType,
errorCode = errorCode,
)
}
}
private fun createDetailsErrorEvent(
error: ApiResponseError,
request: MarketsDataAnalyticsEvent.Details.Error.Request,
tokenSymbol: String,
): MarketsDataAnalyticsEvent.Details.Error {
return createErrorEvent(error) { errorType, errorCode ->
MarketsDataAnalyticsEvent.Details.Error(
errorType = errorType,
errorCode = errorCode,
request = request,
tokenSymbol = tokenSymbol,
)
}
}
private inline fun <T> createErrorEvent(
error: ApiResponseError,
createEvent: (MarketsDataAnalyticsEvent.Type, Int?) -> T,
): T {
return when (error) {
is ApiResponseError.HttpException -> {
createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.code)
}
is ApiResponseError.TimeoutException -> {
createEvent(MarketsDataAnalyticsEvent.Type.Timeout, null)
}
is ApiResponseError.NetworkException -> {
createEvent(MarketsDataAnalyticsEvent.Type.Network, null)
}
is ApiResponseError.UnknownException -> {
createEvent(MarketsDataAnalyticsEvent.Type.Unknown, null)
}
}
}
}

View file

@ -25,7 +25,7 @@ internal class MarketsBatchUpdateFetcher(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onApiError: () -> Unit,
private val onApiError: (ApiResponseError) -> Unit,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
@ -124,6 +124,7 @@ internal class MarketsBatchUpdateFetcher(
analyticsEventHandler.send(
MarketsDataAnalyticsEvent.ChartNullValuesError(
requestPath = "coins/history_preview",
errorType = MarketsDataAnalyticsEvent.Type.Custom,
),
)
@ -133,11 +134,11 @@ internal class MarketsBatchUpdateFetcher(
}
}
private inline fun <T> catchApiError(onError: () -> Unit, block: () -> T): T {
private inline fun <T> catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T {
return try {
block()
} catch (e: ApiResponseError) {
onError()
onError(e)
throw e
}
}

View file

@ -9,7 +9,16 @@ sealed interface MarketsDataAnalyticsEvent {
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets", event = event, params = params), MarketsDataAnalyticsEvent {
data object Error : List(event = "Data Error")
data class Error(
val errorType: Type,
val errorCode: Int? = null,
) : List(
event = "Data Error",
params = buildMap {
put("Error Type", errorType.value)
errorCode?.let { put("Error Code", it.toString()) }
},
)
}
sealed class Details(
@ -20,12 +29,16 @@ sealed interface MarketsDataAnalyticsEvent {
data class Error(
val request: Request,
val tokenSymbol: String,
val errorType: Type,
val errorCode: Int? = null,
) : Details(
event = "Data Error",
params = mapOf(
"Source" to request.source,
"Token" to tokenSymbol,
),
params = buildMap {
put("Source", request.source)
put("Token", tokenSymbol)
errorCode?.let { put("Error Code", it.toString()) }
put("Error Type", errorType.value)
},
) {
enum class Request(val source: String) {
@ -43,13 +56,25 @@ sealed interface MarketsDataAnalyticsEvent {
data class ChartNullValuesError(
val requestPath: String,
val errorType: Type,
val errorCode: Int? = null,
) : AnalyticsEvent(
category = "Markets / Chart",
event = "Data Error",
params = mapOf("Request path" to requestPath),
error = IllegalStateException(
"Chart data contains null values from the API",
),
params = buildMap {
put("Request path", requestPath)
errorCode?.let { put("Error Code", it.toString()) }
put("Error Type", errorType.value)
put("Error Description", "Chart data contains null values from the API")
},
),
MarketsDataAnalyticsEvent
enum class Type(val value: String) {
Http("Http"),
Timeout("Timeout"),
Network("Network"),
Custom("Custom"),
Unknown("Unknown"),
}
}