diff --git a/buildSrc/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/buildSrc/src/main/kotlin/Plugins.kt index 566216bc450a5a609d1efea6e3653cc3f0f622e5..f8343ee68b5ea9e001fcb63f95a4246ea2c34a98 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 433f163d476493c3df9fcd1c046ca7881b880bb1..77a9cba1639b8f07d9d15854b28daa0b56b17258 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 11a7b218e3f90932ea64871a49b2266eada7cb39..ea72e36beb91bafd6fe249b51d3edc2e91af7992 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 f0ba92d5ab593da574ef130ae299fb6a661a54ed..ef98515a8e2699634fa90e609fd76eb5ea055356 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,12 +21,15 @@ 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 kotlinx.coroutines.delay +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 { @@ -45,6 +48,18 @@ object KtorEtherClient { } } } + install(HttpRequestRetry) { + maxRetries = 3 + retryIf { _, response -> + !response.status.isSuccess() + } + retryOnExceptionIf { _, _ -> + true + } + delayMillis { retry -> + retry * 3000L + } + } install(HttpTimeout) { requestTimeoutMillis = 100000 connectTimeoutMillis = 100000 @@ -73,13 +88,43 @@ object KtorEtherClient { block: HttpRequestBuilder.() -> Unit = {}, ): Either = try { - 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() + 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.captureMessage(e.message ?: "Error in securityResponse") Either.Error(ErrorResponse(code = 0, description = e.message ?: "Error")) } + + /** + * Retry mechanism for unhandled exceptions + * @param retries number of retries + * @param delayMs delay between retries in milliseconds + * @param block suspended operation to retry + */ + suspend inline fun retryApiCall( + retries: Int = 3, + delayMs: Long = 3000L, + block: () -> T + ): T { + var currentDelay = delayMs + repeat(retries - 1) { + try { + return block() + } catch (e: Exception) { + delay(delayMs) + currentDelay += delayMs + } + } + return try { + block() + } catch (e: Exception) { + throw Exception("Failed in retry: ${e.message}") + } + } } \ No newline at end of file diff --git a/tabletApp/build.gradle.kts b/tabletApp/build.gradle.kts index 91620a94a12a35765370b2f7ec31d28716dc52d4..11a012504270697ecc9a1a20da81a41041c5a8df 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/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 c15662a35fde13d7c747fddd0eb8ad1d1ce35a79..8f94cdd9f0321865ae848e76401ce0c215102639 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( diff --git a/tabletApp/features/roomInfo/build.gradle.kts b/tabletApp/features/roomInfo/build.gradle.kts index 09fcb5569b842fd64517f64c0adc5428281c6ee7..c0a34318018ee136a825b257627d6f74bae1b880 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 6c680d0a4396a684927e3d4bf2b5524822c984d4..c0596f8a5358cc3be3f978dc258f37800cbd0497 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,9 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { runKioskMode() super.onCreate(savedInstanceState) + if (BuildConfig.BUILD_TYPE.contentEquals("release")) { + 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 0000000000000000000000000000000000000000..dc9bb618de183903fdf50027df84d2b9572f2198 --- /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 + } +}