From f313beccb5a3bd19ecd4326f6995ecd77c550d93 Mon Sep 17 00:00:00 2001 From: TheGood77 Date: Wed, 22 Jan 2025 13:44:45 +0600 Subject: [PATCH 1/4] [+] add Sentry for catch errors and add RequestRetry --- buildSrc/buildSrc/src/main/kotlin/Plugins.kt | 7 ++++++- contract/build.gradle.kts | 1 + .../effective/office/utils/KtorEitherPlagin.kt | 8 ++++++++ .../effective/office/utils/KtorEtherClient.kt | 18 ++++++++++++++++-- tabletApp/build.gradle.kts | 5 +++++ tabletApp/features/roomInfo/build.gradle.kts | 1 + .../effective/office/tablet/MainActivity.kt | 1 + .../effective/office/tablet/SentrySetup.kt | 12 ++++++++++++ 8 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 tabletApp/src/commonMain/kotlin/band/effective/office/tablet/SentrySetup.kt diff --git a/buildSrc/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/buildSrc/src/main/kotlin/Plugins.kt index 566216bc..f8343ee6 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Plugins.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Plugins.kt @@ -71,8 +71,13 @@ object Plugins { const val plugin = "app.cash.sqldelight" } - object GoogleServices{ + object GoogleServices { const val implementation = "com.google.gms:google-services:4.3.15" const val plugin = "com.google.gms.google-services" } + + object Sentry { + const val version = "0.10.0" + const val plugin = "io.sentry.kotlin.multiplatform.gradle" + } } \ No newline at end of file diff --git a/contract/build.gradle.kts b/contract/build.gradle.kts index 433f163d..77a9cba1 100644 --- a/contract/build.gradle.kts +++ b/contract/build.gradle.kts @@ -7,6 +7,7 @@ plugins { id(Plugins.BuildConfig.plugin) id(Plugins.Serialization.plugin) id(Plugins.CocoaPods.plugin) + id(Plugins.Sentry.plugin) version Plugins.Sentry.version } android { diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt index 11a7b218..ea72e36b 100644 --- a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEitherPlagin.kt @@ -6,6 +6,7 @@ import band.effective.office.network.model.ErrorResponse import io.ktor.client.plugins.api.createClientPlugin import io.ktor.client.statement.request import io.ktor.utils.io.readUTF8Line +import io.sentry.kotlin.multiplatform.Sentry import kotlinx.serialization.json.Json import kotlinx.serialization.serializer @@ -32,6 +33,13 @@ val KtorEitherPlugin = createClientPlugin("KtorEitherPlugin") { "KtorEitherPluginError: ${response.request.method} ${response.request.url} ${response.status.value}: ${response.status.description}\n" + "${content.readUTF8Line()}" ) + val errorMessage = """ + KtorEitherPluginError: ${response.request.method} ${response.request.url} + Status: ${response.status.value} - ${response.status.description} + Response Body: ${content.readUTF8Line()} + """.trimIndent() + // For send to Sentry + Sentry.captureMessage(errorMessage) Either.Error(ErrorResponse.getResponse(response.status.value)) } } diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt index f0ba92d5..70c2e401 100644 --- a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt @@ -6,7 +6,7 @@ import band.effective.office.network.model.ErrorResponse import effective_office.contract.BuildConfig import io.ktor.client.HttpClient import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.auth.Auth import io.ktor.client.plugins.auth.providers.BearerTokens @@ -21,8 +21,9 @@ import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.put -import io.ktor.http.HttpHeaders +import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json +import io.sentry.kotlin.multiplatform.Sentry object KtorEtherClient { /**token for authorization*/ @@ -56,6 +57,18 @@ object KtorEtherClient { logger = Logger.DEFAULT level = LogLevel.ALL } + install(HttpRequestRetry) { + maxRetries = 3 + retryIf { _, response -> + !response.status.isSuccess() + } + retryOnExceptionIf { _, _ -> + true + } + delayMillis { retry -> + retry * 3000L + } + } } } @@ -80,6 +93,7 @@ object KtorEtherClient { RestMethod.Put -> client.put(urlString, block) }.body() } catch (e: Exception) { + Sentry.captureException(e) Either.Error(ErrorResponse(code = 0, description = e.message ?: "Error")) } } \ No newline at end of file diff --git a/tabletApp/build.gradle.kts b/tabletApp/build.gradle.kts index 91620a94..11a01250 100644 --- a/tabletApp/build.gradle.kts +++ b/tabletApp/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { id(Plugins.Android.plugin) id(Plugins.MultiplatformCompose.plugin) @@ -5,8 +7,10 @@ plugins { id(Plugins.Parcelize.plugin) id(Plugins.Libres.plugin) id(Plugins.GoogleServices.plugin) + id(Plugins.Sentry.plugin) version Plugins.Sentry.version } +val sentryTabletDsnUrl: String = gradleLocalProperties(rootDir).getProperty("sentryTabletDsnUrl") android { namespace = "band.effective.office.tablet" compileSdk = 34 @@ -18,6 +22,7 @@ android { minSdk = 26 targetSdk = 34 + buildConfigField("String", "sentryTabletDsnUrl", sentryTabletDsnUrl) } sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") diff --git a/tabletApp/features/roomInfo/build.gradle.kts b/tabletApp/features/roomInfo/build.gradle.kts index 09fcb556..c0a34318 100644 --- a/tabletApp/features/roomInfo/build.gradle.kts +++ b/tabletApp/features/roomInfo/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id(Plugins.Kotlin.plugin) id(Plugins.Parcelize.plugin) id(Plugins.Libres.plugin) + id(Plugins.Sentry.plugin) version Plugins.Sentry.version } android { diff --git a/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt b/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt index 6c680d0a..d4aa4da6 100644 --- a/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt +++ b/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt @@ -54,6 +54,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { runKioskMode() super.onCreate(savedInstanceState) + initializeSentry() SharedPref.sharedPref.init(this) setContent { App(defaultComponentContext(), DefaultStoreFactory()) diff --git a/tabletApp/src/commonMain/kotlin/band/effective/office/tablet/SentrySetup.kt b/tabletApp/src/commonMain/kotlin/band/effective/office/tablet/SentrySetup.kt new file mode 100644 index 00000000..dc9bb618 --- /dev/null +++ b/tabletApp/src/commonMain/kotlin/band/effective/office/tablet/SentrySetup.kt @@ -0,0 +1,12 @@ +package band.effective.office.tablet + +import io.sentry.kotlin.multiplatform.Sentry + +fun initializeSentry() { + Sentry.init { options -> + options.dsn = BuildConfig.sentryTabletDsnUrl + + options.experimental.sessionReplay.onErrorSampleRate = 1.0 + options.experimental.sessionReplay.sessionSampleRate = 1.0 + } +} -- GitLab From 68a03e405636069df666849817feffd451faba5b Mon Sep 17 00:00:00 2001 From: TheGood77 Date: Thu, 20 Feb 2025 11:35:42 +0600 Subject: [PATCH 2/4] [+] add retry mechanism for unhandled exceptions --- .../effective/office/utils/KtorEtherClient.kt | 68 ++++++++++++++----- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt index 70c2e401..5c01f322 100644 --- a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt @@ -5,7 +5,6 @@ import band.effective.office.network.model.Either import band.effective.office.network.model.ErrorResponse import effective_office.contract.BuildConfig import io.ktor.client.HttpClient -import io.ktor.client.call.body import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.auth.Auth @@ -21,13 +20,17 @@ import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.put +import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.delay +import kotlinx.serialization.json.Json import io.sentry.kotlin.multiplatform.Sentry object KtorEtherClient { /**token for authorization*/ var token = mutableListOf(BuildConfig.apiKey) + /**default http client with KtorEtherClient*/ val httpClient by lazy { @@ -46,17 +49,6 @@ object KtorEtherClient { } } } - install(HttpTimeout) { - requestTimeoutMillis = 100000 - connectTimeoutMillis = 100000 - } - install(ContentNegotiation) { - json() - } - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.ALL - } install(HttpRequestRetry) { maxRetries = 3 retryIf { _, response -> @@ -69,6 +61,17 @@ object KtorEtherClient { retry * 3000L } } + install(HttpTimeout) { + requestTimeoutMillis = 100000 + connectTimeoutMillis = 100000 + } + install(ContentNegotiation) { + json() + } + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.ALL + } } } @@ -82,18 +85,49 @@ object KtorEtherClient { suspend inline fun securityResponse( urlString: String, method: RestMethod = RestMethod.Get, + maxRetries: Int = 3, client: HttpClient = httpClient, block: HttpRequestBuilder.() -> Unit = {}, - ): Either = + ): Either = withRetry(maxRetries) { try { - when (method) { + val response = when (method) { RestMethod.Get -> client.get(urlString, block) RestMethod.Post -> client.post(urlString, block) RestMethod.Delete -> client.delete(urlString, block) RestMethod.Put -> client.put(urlString, block) - }.body() + }.bodyAsText() + Json.decodeFromString(response) } catch (e: Exception) { - Sentry.captureException(e) - Either.Error(ErrorResponse(code = 0, description = e.message ?: "Error")) + Sentry.captureException(e.message) + throw Exception("Failed to parse response: ${e.message}") } + } + + /** + * Retry mechanism for unhandled exceptions + * @param maxRetries maximum number of retries + * @param delayMs delay between retries in milliseconds + * @param block suspended operation to retry + */ + suspend inline fun withRetry( + maxRetries: Int, + delayMs: Long = 3000L, + block: () -> T + ): Either { + var currentTry = 0 + var lastException: Exception? = null + + while (currentTry < maxRetries) { + try { + return Either.Success(block()) + } catch (e: Exception) { + lastException = e + currentTry++ + if (currentTry < maxRetries) { + delay(delayMs * currentTry) + } + } + } + return Either.Error(ErrorResponse(code = 0, description = lastException?.message ?: "Error")) + } } \ No newline at end of file -- GitLab From 1dbc2a3d4a3259bb4c4b67eadf27b7ae6994c657 Mon Sep 17 00:00:00 2001 From: TheGood77 Date: Thu, 13 Mar 2025 09:21:03 +0600 Subject: [PATCH 3/4] [~] change retry mechanism --- .../effective/office/utils/KtorEtherClient.kt | 55 +++++++++---------- .../repository/impl/NetworkEventRepository.kt | 29 ++++++---- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt index 5c01f322..ef98515a 100644 --- a/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt +++ b/contract/src/commonMain/kotlin/band/effective/office/utils/KtorEtherClient.kt @@ -5,6 +5,7 @@ import band.effective.office.network.model.Either import band.effective.office.network.model.ErrorResponse import effective_office.contract.BuildConfig import io.ktor.client.HttpClient +import io.ktor.client.call.body import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.auth.Auth @@ -20,11 +21,9 @@ import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.put -import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.delay -import kotlinx.serialization.json.Json import io.sentry.kotlin.multiplatform.Sentry object KtorEtherClient { @@ -85,49 +84,47 @@ object KtorEtherClient { suspend inline fun securityResponse( urlString: String, method: RestMethod = RestMethod.Get, - maxRetries: Int = 3, client: HttpClient = httpClient, block: HttpRequestBuilder.() -> Unit = {}, - ): Either = withRetry(maxRetries) { + ): Either = try { - val response = when (method) { - RestMethod.Get -> client.get(urlString, block) - RestMethod.Post -> client.post(urlString, block) - RestMethod.Delete -> client.delete(urlString, block) - RestMethod.Put -> client.put(urlString, block) - }.bodyAsText() - Json.decodeFromString(response) + retryApiCall { + when (method) { + RestMethod.Get -> client.get(urlString, block) + RestMethod.Post -> client.post(urlString, block) + RestMethod.Delete -> client.delete(urlString, block) + RestMethod.Put -> client.put(urlString, block) + }.body() + } } catch (e: Exception) { - Sentry.captureException(e.message) - throw Exception("Failed to parse response: ${e.message}") + Sentry.captureMessage(e.message ?: "Error in securityResponse") + Either.Error(ErrorResponse(code = 0, description = e.message ?: "Error")) } - } /** * Retry mechanism for unhandled exceptions - * @param maxRetries maximum number of retries + * @param retries number of retries * @param delayMs delay between retries in milliseconds * @param block suspended operation to retry */ - suspend inline fun withRetry( - maxRetries: Int, + suspend inline fun retryApiCall( + retries: Int = 3, delayMs: Long = 3000L, block: () -> T - ): Either { - var currentTry = 0 - var lastException: Exception? = null - - while (currentTry < maxRetries) { + ): T { + var currentDelay = delayMs + repeat(retries - 1) { try { - return Either.Success(block()) + return block() } catch (e: Exception) { - lastException = e - currentTry++ - if (currentTry < maxRetries) { - delay(delayMs * currentTry) - } + delay(delayMs) + currentDelay += delayMs } } - return Either.Error(ErrorResponse(code = 0, description = lastException?.message ?: "Error")) + return try { + block() + } catch (e: Exception) { + throw Exception("Failed in retry: ${e.message}") + } } } \ No newline at end of file diff --git a/tabletApp/features/network/src/commonMain/kotlin/band/effective/office/tablet/network/repository/impl/NetworkEventRepository.kt b/tabletApp/features/network/src/commonMain/kotlin/band/effective/office/tablet/network/repository/impl/NetworkEventRepository.kt index c15662a3..8f94cdd9 100644 --- a/tabletApp/features/network/src/commonMain/kotlin/band/effective/office/tablet/network/repository/impl/NetworkEventRepository.kt +++ b/tabletApp/features/network/src/commonMain/kotlin/band/effective/office/tablet/network/repository/impl/NetworkEventRepository.kt @@ -35,14 +35,15 @@ class NetworkEventRepository( } val finish = GregorianCalendar().apply { add(Calendar.DAY_OF_MONTH, 14) } val response = api.getWorkspacesWithBookings( - tag = "meeting", - freeFrom = start.timeInMillis, - freeUntil = finish.timeInMillis - ) + tag = "meeting", + freeFrom = start.timeInMillis, + freeUntil = finish.timeInMillis + ) return when (response) { is Either.Error -> { Either.Error(ErrorWithData(response.error, null)) } + is Either.Success -> { Either.Success(response.data.map { it.toRoom() }) } @@ -67,7 +68,10 @@ class NetworkEventRepository( room: RoomInfo, ): Either = api.deleteBooking(eventInfo.id) - .map({ it }, { "ok" }) + .map( + errorMapper = { it }, + successMapper = { "ok" }, + ) override suspend fun getBooking( eventInfo: EventInfo @@ -84,13 +88,14 @@ class NetworkEventRepository( .map { Either.Success(emptyList()) } /**Map domain model to DTO*/ - private fun EventInfo.toBookingRequestDTO(room: RoomInfo): BookingRequestDTO = BookingRequestDTO( - beginBooking = this.startTime.timeInMillis, - endBooking = this.finishTime.timeInMillis, - ownerEmail = this.organizer.email, - participantEmails = listOfNotNull(this.organizer.email), - workspaceId = room.id - ) + private fun EventInfo.toBookingRequestDTO(room: RoomInfo): BookingRequestDTO = + BookingRequestDTO( + beginBooking = this.startTime.timeInMillis, + endBooking = this.finishTime.timeInMillis, + ownerEmail = this.organizer.email, + participantEmails = listOfNotNull(this.organizer.email), + workspaceId = room.id + ) /**Map DTO to domain model*/ private fun BookingResponseDTO.toEventInfo(): EventInfo = EventInfo( -- GitLab From f2185267370e3a9ee4505defc8fd56631e6aa782 Mon Sep 17 00:00:00 2001 From: TheGood77 Date: Thu, 13 Mar 2025 09:21:42 +0600 Subject: [PATCH 4/4] [+] add Sentry in release build --- .../kotlin/band/effective/office/tablet/MainActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt b/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt index d4aa4da6..c0596f8a 100644 --- a/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt +++ b/tabletApp/src/androidMain/kotlin/band/effective/office/tablet/MainActivity.kt @@ -54,7 +54,9 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { runKioskMode() super.onCreate(savedInstanceState) - initializeSentry() + if (BuildConfig.BUILD_TYPE.contentEquals("release")) { + initializeSentry() + } SharedPref.sharedPref.init(this) setContent { App(defaultComponentContext(), DefaultStoreFactory()) -- GitLab