From d995b70889124064e47c013966128e7dd8bc6099 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Wed, 11 Oct 2023 15:28:25 +0400 Subject: [PATCH 1/3] Implement Sentry transactions convenient api --- .../home/presentation/HomeActionDispatcher.kt | 60 +++++++-------- .../ProgressScreenActionDispatcher.kt | 67 ++++++---------- .../ProjectSelectionListActionDispatcher.kt | 76 ++++++------------- .../app/sentry/domain/SentryExtentions.kt | 28 +++++++ 4 files changed, 101 insertions(+), 130 deletions(-) create mode 100644 shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt index 2f82d1d18e..62d44a115e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt @@ -3,6 +3,7 @@ package org.hyperskill.app.home.presentation import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn @@ -25,6 +26,7 @@ import org.hyperskill.app.home.presentation.HomeFeature.Message import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder +import org.hyperskill.app.sentry.domain.withTransaction import org.hyperskill.app.step.domain.interactor.StepInteractor import org.hyperskill.app.topics_repetitions.domain.flow.TopicRepeatedFlow import org.hyperskill.app.topics_repetitions.domain.interactor.TopicsRepetitionsInteractor @@ -69,39 +71,7 @@ class HomeActionDispatcher( override suspend fun doSuspendableAction(action: Action) { when (action) { - is Action.FetchHomeScreenData -> { - val sentryTransaction = HyperskillSentryTransactionBuilder.buildHomeScreenRemoteDataLoading() - sentryInteractor.startTransaction(sentryTransaction) - - val currentProfile = currentProfileStateRepository - .getState(forceUpdate = true) // ALTAPPS-303: Get from remote to get relevant problem of the day - .getOrElse { - sentryInteractor.finishTransaction(sentryTransaction, throwable = it) - return onNewMessage(Message.HomeFailure) - } - - val problemOfDayStateResult = actionScope.async { getProblemOfDayState(currentProfile.dailyStep) } - val repetitionsStateResult = actionScope.async { getRepetitionsState() } - val isFreemiumEnabledResult = actionScope.async { freemiumInteractor.isFreemiumEnabled() } - - val problemOfDayState = problemOfDayStateResult.await().getOrElse { - sentryInteractor.finishTransaction(sentryTransaction, throwable = it) - return onNewMessage(Message.HomeFailure) - } - val repetitionsState = repetitionsStateResult.await().getOrElse { - sentryInteractor.finishTransaction(sentryTransaction, throwable = it) - return onNewMessage(Message.HomeFailure) - } - val isFreemiumEnabled = isFreemiumEnabledResult.await().getOrElse { - sentryInteractor.finishTransaction(sentryTransaction, throwable = it) - return onNewMessage(Message.HomeFailure) - } - - sentryInteractor.finishTransaction(sentryTransaction) - - onNewMessage(Message.HomeSuccess(problemOfDayState, repetitionsState, isFreemiumEnabled)) - onNewMessage(Message.ReadyToLaunchNextProblemInTimer) - } + is Action.FetchHomeScreenData -> fetchHomeScreenData() is Action.LaunchTimer -> { if (isTimerLaunched) { return @@ -137,6 +107,30 @@ class HomeActionDispatcher( } } + private suspend fun fetchHomeScreenData() { + sentryInteractor.withTransaction( + HyperskillSentryTransactionBuilder.buildHomeScreenRemoteDataLoading(), + onError = { setOf(Message.HomeFailure) } + ) { + coroutineScope { + val currentProfile = currentProfileStateRepository + .getState(forceUpdate = true) // ALTAPPS-303: Get from remote to get a relevant problem of the day + .getOrThrow() + val problemOfDayStateResult = async { getProblemOfDayState(currentProfile.dailyStep) } + val repetitionsStateResult = async { getRepetitionsState() } + val isFreemiumEnabledResult = async { freemiumInteractor.isFreemiumEnabled() } + setOf( + Message.HomeSuccess( + problemOfDayState = problemOfDayStateResult.await().getOrThrow(), + repetitionsState = repetitionsStateResult.await().getOrThrow(), + isFreemiumEnabled = isFreemiumEnabledResult.await().getOrThrow() + ), + Message.ReadyToLaunchNextProblemInTimer + ) + } + }.forEach(::onNewMessage) + } + private suspend fun getProblemOfDayState(dailyStepId: Long?): Result = if (dailyStepId == null) { Result.success(HomeFeature.ProblemOfDayState.Empty) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/progress_screen/presentation/ProgressScreenActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/progress_screen/presentation/ProgressScreenActionDispatcher.kt index 11cef7a323..93e73f713d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/progress_screen/presentation/ProgressScreenActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/progress_screen/presentation/ProgressScreenActionDispatcher.kt @@ -12,6 +12,7 @@ import org.hyperskill.app.projects.domain.model.ProjectWithProgress import org.hyperskill.app.projects.domain.repository.ProjectsRepository import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder +import org.hyperskill.app.sentry.domain.withTransaction import org.hyperskill.app.study_plan.domain.repository.CurrentStudyPlanStateRepository import org.hyperskill.app.track.domain.interactor.TrackInteractor import org.hyperskill.app.track.domain.model.TrackWithProgress @@ -50,59 +51,39 @@ internal class ProgressScreenActionDispatcher( action: InternalAction.FetchTrackWithProgress, onNewMessage: (Message) -> Unit ) { - coroutineScope { - val transaction = HyperskillSentryTransactionBuilder - .buildProgressScreenRemoteTrackWithProgressLoading() - sentryInteractor.startTransaction(transaction) - - val profileDeferred = async { - currentProfileStateRepository - .getState(forceUpdate = action.forceLoadFromNetwork) - } - val studyPlanDeferred = async { - currentStudyPlanStateRepository - .getState(forceUpdate = action.forceLoadFromNetwork) - } - - val profile = profileDeferred.await().getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProgressScreenFeature.TrackWithProgressFetchResult.Error) - return@coroutineScope - } - val studyPlan = studyPlanDeferred.await().getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProgressScreenFeature.TrackWithProgressFetchResult.Error) - return@coroutineScope - } + sentryInteractor.withTransaction( + HyperskillSentryTransactionBuilder.buildProgressScreenRemoteTrackWithProgressLoading(), + onError = { ProgressScreenFeature.TrackWithProgressFetchResult.Error } + ) { + coroutineScope { + val profileDeferred = async { + currentProfileStateRepository + .getState(forceUpdate = action.forceLoadFromNetwork) + } + val studyPlanDeferred = async { + currentStudyPlanStateRepository + .getState(forceUpdate = action.forceLoadFromNetwork) + } - if (studyPlan.trackId == null) { - sentryInteractor.finishTransaction(transaction) - onNewMessage(ProgressScreenFeature.TrackWithProgressFetchResult.Error) - return@coroutineScope - } + val profile = profileDeferred.await().getOrThrow() + val studyPlan = studyPlanDeferred.await().getOrThrow() - val trackWithProgress = fetchTrackWithProgress(studyPlan.trackId, action.forceLoadFromNetwork) - .getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProgressScreenFeature.TrackWithProgressFetchResult.Error) - return@coroutineScope + if (studyPlan.trackId == null) { + return@coroutineScope ProgressScreenFeature.TrackWithProgressFetchResult.Error } - if (trackWithProgress == null) { - sentryInteractor.finishTransaction(transaction) - onNewMessage(ProgressScreenFeature.TrackWithProgressFetchResult.Error) - return@coroutineScope - } + val trackWithProgress = + fetchTrackWithProgress(studyPlan.trackId, action.forceLoadFromNetwork) + .getOrThrow() + ?: return@coroutineScope ProgressScreenFeature.TrackWithProgressFetchResult.Error - sentryInteractor.finishTransaction(transaction) - onNewMessage( ProgressScreenFeature.TrackWithProgressFetchResult.Success( trackWithProgress = trackWithProgress, studyPlan = studyPlan, profile = profile ) - ) - } + } + }.let(onNewMessage) } private suspend fun handleFetchProjectWithProgressAction( diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/project_selection/list/presentation/ProjectSelectionListActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/project_selection/list/presentation/ProjectSelectionListActionDispatcher.kt index 6ef64541c9..1790be6034 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/project_selection/list/presentation/ProjectSelectionListActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/project_selection/list/presentation/ProjectSelectionListActionDispatcher.kt @@ -14,6 +14,7 @@ import org.hyperskill.app.projects.domain.model.projectId import org.hyperskill.app.projects.domain.repository.ProjectsRepository import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder +import org.hyperskill.app.sentry.domain.withTransaction import org.hyperskill.app.study_plan.domain.repository.CurrentStudyPlanStateRepository import org.hyperskill.app.track.domain.model.getAllProjects import org.hyperskill.app.track.domain.repository.TrackRepository @@ -49,64 +50,31 @@ internal class ProjectSelectionListActionDispatcher( action: InternalAction.FetchContent, onNewMessage: (Message) -> Unit ) { - coroutineScope { - val transaction = HyperskillSentryTransactionBuilder - .buildProjectSelectionListScreenRemoteDataLoading() - sentryInteractor.startTransaction(transaction) + sentryInteractor.withTransaction( + HyperskillSentryTransactionBuilder.buildProjectSelectionListScreenRemoteDataLoading(), + onError = { ProjectSelectionListFeature.ContentFetchResult.Error } + ) { + coroutineScope { + val profile = currentProfileStateRepository.getState(forceUpdate = false).getOrThrow() + val track = trackRepository.getTrack(action.trackId, action.forceLoadFromNetwork).getOrThrow() - val profile = currentProfileStateRepository.getState(forceUpdate = false) - .getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProjectSelectionListFeature.ContentFetchResult.Error) - return@coroutineScope - } - - val track = - trackRepository.getTrack(action.trackId, action.forceLoadFromNetwork) - .getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProjectSelectionListFeature.ContentFetchResult.Error) - return@coroutineScope - } - - val projectsIds = track.getAllProjects(profile.isBeta) - - val studyPlanDeferred = async { - currentStudyPlanStateRepository.getState(action.forceLoadFromNetwork) - } - - val projectsDeferred = async { - projectsRepository.getProjects(projectsIds, action.forceLoadFromNetwork) - } + val projectsIds = track.getAllProjects(profile.isBeta) - val projectsProgressesDeferred = async { - progressesRepository.getProjectsProgresses(projectsIds, action.forceLoadFromNetwork) - } - - val studyPlan = studyPlanDeferred.await() - .getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProjectSelectionListFeature.ContentFetchResult.Error) - return@coroutineScope + val studyPlanDeferred = async { + currentStudyPlanStateRepository.getState(action.forceLoadFromNetwork) } - - val projects = projectsDeferred.await() - .getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProjectSelectionListFeature.ContentFetchResult.Error) - return@coroutineScope + val projectsDeferred = async { + projectsRepository.getProjects(projectsIds, action.forceLoadFromNetwork) + } + val projectsProgressesDeferred = async { + progressesRepository.getProjectsProgresses(projectsIds, action.forceLoadFromNetwork) } - val projectsProgresses: Map = - projectsProgressesDeferred.await().map(::mapProgressesToMap) - .getOrElse { - sentryInteractor.finishTransaction(transaction, throwable = it) - onNewMessage(ProjectSelectionListFeature.ContentFetchResult.Error) - return@coroutineScope - } + val studyPlan = studyPlanDeferred.await().getOrThrow() + val projects = projectsDeferred.await().getOrThrow() + val projectsProgresses: Map = + projectsProgressesDeferred.await().map(::mapProgressesToMap).getOrThrow() - sentryInteractor.finishTransaction(transaction) - onNewMessage( ProjectSelectionListFeature.ContentFetchResult.Success( profile = profile, track = track, @@ -119,8 +87,8 @@ internal class ProjectSelectionListActionDispatcher( }, currentProjectId = studyPlan.projectId ) - ) - } + } + }.let(onNewMessage) } private fun mapProgressesToMap(progresses: List): Map = diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt new file mode 100644 index 0000000000..7519edd673 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.sentry.domain + +import org.hyperskill.app.sentry.domain.interactor.SentryInteractor +import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransaction + +/** + * Execute a block of code within a Sentry transaction. + * + * @param transaction The transaction to be started. + * @param onError Callback function to create a result in case of an error. + * @param measureBlock The block of code to be executed within the transaction. + * @return The measureBlock execution result. + */ +suspend inline fun SentryInteractor.withTransaction( + transaction: HyperskillSentryTransaction, + onError: (Throwable) -> T, + measureBlock: () -> T +): T { + startTransaction(transaction) + return try { + val result = measureBlock() + finishTransaction(transaction) + result + } catch (e: Exception) { + finishTransaction(transaction, e) + onError(e) + } +} \ No newline at end of file From 19de95f58df3d7cc783d455f59fd36b2b41d4148 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Wed, 11 Oct 2023 15:30:55 +0400 Subject: [PATCH 2/3] Update docs --- .../kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt index 7519edd673..d69f54c63e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/SentryExtentions.kt @@ -5,6 +5,7 @@ import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransa /** * Execute a block of code within a Sentry transaction. + * Automatically starts and finish the [transaction]. * * @param transaction The transaction to be started. * @param onError Callback function to create a result in case of an error. From 53660eb1873fac096a08c17a9658bb5b180eb974 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Wed, 11 Oct 2023 17:53:02 +0400 Subject: [PATCH 3/3] Fix review issues --- .../app/home/presentation/HomeActionDispatcher.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt index 62d44a115e..5d4e5d748e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt @@ -71,7 +71,7 @@ class HomeActionDispatcher( override suspend fun doSuspendableAction(action: Action) { when (action) { - is Action.FetchHomeScreenData -> fetchHomeScreenData() + is Action.FetchHomeScreenData -> handleFetchHomeScreenData(::onNewMessage) is Action.LaunchTimer -> { if (isTimerLaunched) { return @@ -107,7 +107,7 @@ class HomeActionDispatcher( } } - private suspend fun fetchHomeScreenData() { + private suspend fun handleFetchHomeScreenData(onNewMessage: (Message) -> Unit) { sentryInteractor.withTransaction( HyperskillSentryTransactionBuilder.buildHomeScreenRemoteDataLoading(), onError = { setOf(Message.HomeFailure) } @@ -128,7 +128,7 @@ class HomeActionDispatcher( Message.ReadyToLaunchNextProblemInTimer ) } - }.forEach(::onNewMessage) + }.forEach(onNewMessage) } private suspend fun getProblemOfDayState(dailyStepId: Long?): Result =