From 98257dbad55c2a9a03f898a38fa7ec57b27713a5 Mon Sep 17 00:00:00 2001 From: "Mr. 17" Date: Wed, 8 May 2024 21:34:43 +0530 Subject: [PATCH] Fix #4042: Implement success criteria metrics for lesson checkpointing (#5336) ## Explanation Fixes #4042 This PR implements the success criteria metrics required for lesson checkpointing as described [here](https://docs.google.com/document/d/1d8yjwz76mngtsPRxC7fubgLKg8mfA7kG1sWRWdbiaVw/edit#bookmark=id.2zyjd5vygmcv). A high level overview of the implementation is documented on the issue thread [here](https://github.com/oppia/oppia-android/issues/4042#issuecomment-1929799546). #### Changes to FakeAnalyticsEventLogger - `fun getOldestEvents(count: Int): List` is necessary to retrieve a list of a pre-defined count of oldest events from all the logged events. - `fun getLoggedEvent(predicate: (EventLog) -> Boolean): EventLog?` serves to acquire a reference to a logged event when the context of the event is known, but the event index is uncertain or potentially surrounded by other events. Thus, extracting the event via index might be difficult. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only If your PR includes UI-related changes, then: - Add screenshots for portrait/landscape for both a tablet & phone of the before & after UI changes - For the screenshots above, include both English and pseudo-localized (RTL) screenshots (see [RTL guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines)) - Add a video showing the full UX flow with a screen reader enabled (see [accessibility guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide)) - For PRs introducing new UI elements or color changes, both light and dark mode screenshots must be included - Add a screenshot demonstrating that you ran affected Espresso tests locally & that they're passing --------- Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> --- .../ExplorationActivityPresenter.kt | 6 +- .../exploration/ExplorationActivityTest.kt | 128 ++++++++++ .../ExplorationProgressController.kt | 20 ++ .../analytics/LearnerAnalyticsLogger.kt | 50 ++++ .../ExplorationProgressControllerTest.kt | 229 +++++++++++++++--- .../analytics/LearnerAnalyticsLoggerTest.kt | 125 ++++++++++ model/src/main/proto/oppia_logger.proto | 18 ++ .../testing/FakeAnalyticsEventLogger.kt | 6 + .../testing/logging/EventLogSubject.kt | 141 +++++++++++ .../testing/FakeAnalyticsEventLoggerTest.kt | 99 ++++++++ .../util/logging/EventBundleCreator.kt | 15 ++ ...entTypeToHumanReadableNameConverterImpl.kt | 7 + ...entTypeToHumanReadableNameConverterImpl.kt | 7 + 13 files changed, 813 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index e75247720b1..a5464c34459 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -37,6 +37,7 @@ import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ExplorationActivityBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.oppialogger.analytics.LearnerAnalyticsLogger import org.oppia.android.domain.survey.SurveyGatingController import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.accessibility.AccessibilityService @@ -60,6 +61,7 @@ class ExplorationActivityPresenter @Inject constructor( private val fontScaleConfigurationUtil: FontScaleConfigurationUtil, private val translationController: TranslationController, private val oppiaLogger: OppiaLogger, + private val learnerAnalyticsLogger: LearnerAnalyticsLogger, private val resourceHandler: AppLanguageResourceHandler, private val surveyGatingController: SurveyGatingController ) { @@ -328,7 +330,7 @@ class ExplorationActivityPresenter @Inject constructor( return } // If checkpointing is enabled, get the current checkpoint state to show an appropriate dialog - // fragment. + // fragment and log lesson saved advertently event. showDialogFragmentBasedOnCurrentCheckpointState() } @@ -514,9 +516,11 @@ class ExplorationActivityPresenter @Inject constructor( } else { when (checkpointState) { CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT -> { + learnerAnalyticsLogger.explorationAnalyticsLogger.value?.logLessonSavedAdvertently() stopExploration(isCompletion = false) } CheckpointState.CHECKPOINT_SAVED_DATABASE_EXCEEDED_LIMIT -> { + learnerAnalyticsLogger.explorationAnalyticsLogger.value?.logLessonSavedAdvertently() showProgressDatabaseFullDialogFragment() } else -> showUnsavedExplorationDialogFragment() diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 800916c4391..856acf55f80 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -68,6 +68,7 @@ import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.help.HelpActivity +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.LESSON_SAVED_ADVERTENTLY_CONTEXT import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.ProfileId @@ -128,6 +129,7 @@ import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.domain.translation.TranslationController import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.BuildEnvironment +import org.oppia.android.testing.FakeAnalyticsEventLogger import org.oppia.android.testing.OppiaTestRule import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule @@ -222,6 +224,9 @@ class ExplorationActivityTest { @Inject lateinit var fakeAccessibilityService: FakeAccessibilityService + @Inject + lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger + private val internalProfileId: Int = 0 @Before @@ -1932,6 +1937,129 @@ class ExplorationActivityTest { assertThat(explorationActivityTestRule.activity.isFinishing).isTrue() } + @Test + fun testExpActivity_startNewExploration_pressBack_logsLessonSavedAdvertentlyEvent() { + setUpAudioForFractionLesson() + explorationActivityTestRule.launchActivity( + createExplorationActivityIntent( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + shouldSavePartialProgress = true + ) + ) + explorationDataController.startPlayingNewExploration( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + testCoroutineDispatchers.runCurrent() + + pressBack() + testCoroutineDispatchers.runCurrent() + + val lessonSavedAdvertentlyEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == LESSON_SAVED_ADVERTENTLY_CONTEXT + } + assertThat(lessonSavedAdvertentlyEventCount).isEqualTo(1) + } + + @Test + fun testExpActivity_startNewExploration_pressToolbarBackIcon_logsLessonSavedAdvertentlyEvent() { + setUpAudioForFractionLesson() + markAllSpotlightsSeen() + explorationActivityTestRule.launchActivity( + createExplorationActivityIntent( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + shouldSavePartialProgress = true + ) + ) + explorationDataController.startPlayingNewExploration( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + testCoroutineDispatchers.runCurrent() + + // Click on 'X' icon on toolbar. + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)).perform(click()) + testCoroutineDispatchers.runCurrent() + + explorationDataController.stopPlayingExploration(isCompletion = false) + testCoroutineDispatchers.runCurrent() + + val lessonSavedAdvertentlyEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == LESSON_SAVED_ADVERTENTLY_CONTEXT + } + assertThat(lessonSavedAdvertentlyEventCount).isEqualTo(1) + } + + @Test + fun testExpActivity_replayExploration_pressBack_doesNotLogLessonSavedAdvertentlyEvent() { + setUpAudioForFractionLesson() + explorationActivityTestRule.launchActivity( + createExplorationActivityIntent( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + shouldSavePartialProgress = false + ) + ) + explorationDataController.replayExploration( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + testCoroutineDispatchers.runCurrent() + + pressBack() + testCoroutineDispatchers.runCurrent() + + val lessonSavedAdvertentlyEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == LESSON_SAVED_ADVERTENTLY_CONTEXT + } + assertThat(lessonSavedAdvertentlyEventCount).isEqualTo(0) + } + + @Test + fun testExpActivity_replayExp_pressToolbarBackIcon_doesNotLogLessonSavedAdvertentlyEvent() { + setUpAudioForFractionLesson() + markAllSpotlightsSeen() + explorationActivityTestRule.launchActivity( + createExplorationActivityIntent( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + shouldSavePartialProgress = false + ) + ) + explorationDataController.replayExploration( + internalProfileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + testCoroutineDispatchers.runCurrent() + + // Click on 'X' icon on toolbar. + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)).perform(click()) + testCoroutineDispatchers.runCurrent() + + val lessonSavedAdvertentlyEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == LESSON_SAVED_ADVERTENTLY_CONTEXT + } + assertThat(lessonSavedAdvertentlyEventCount).isEqualTo(0) + } + @Test @RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3858): Enable for Espresso. fun testExpActivity_englishContentLang_contentIsInEnglish() { diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt index 4f939fd331c..e2eb046462a 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt @@ -443,6 +443,15 @@ class ExplorationProgressController @Inject constructor( ControllerState( ExplorationProgress(), message.isRestart, + // The [message.explorationCheckpoint] is [ExplorationCheckpoint.getDefaultInstance()] + // in the following 3 cases. + // - New exploration is started. + // - Saved Exploration is restarted. + // - Completed exploration is replayed. + // The [message.explorationCheckpoint] will contain the exploration checkpoint + // only when a saved exploration is resumed. + isResume = message.explorationCheckpoint + != ExplorationCheckpoint.getDefaultInstance(), message.sessionId, message.ephemeralStateFlow, commandQueue, @@ -634,6 +643,14 @@ class ExplorationProgressController @Inject constructor( topPendingState.interaction, userAnswer, answerOutcome.labelledAsCorrectAnswer ) + // Log correct & incorrect answer submission in a resumed exploration. + if (isResume) { + if (answerOutcome.labelledAsCorrectAnswer) + explorationAnalyticsLogger.logResumeLessonSubmitCorrectAnswer() + else + explorationAnalyticsLogger.logResumeLessonSubmitIncorrectAnswer() + } + // Follow the answer's outcome to another part of the graph if it's different. val ephemeralState = computeBaseCurrentEphemeralState() when { @@ -947,9 +964,11 @@ class ExplorationProgressController @Inject constructor( deferred.invokeOnCompletion { val checkpointState = if (it == null) { + explorationAnalyticsLogger.logProgressSavingSuccess() deferred.getCompleted() } else { oppiaLogger.e("Lightweight checkpointing", "Failed to save checkpoint in exploration", it) + explorationAnalyticsLogger.logProgressSavingFailure() // CheckpointState is marked as CHECKPOINT_UNSAVED because the deferred did not // complete successfully. CheckpointState.CHECKPOINT_UNSAVED @@ -1094,6 +1113,7 @@ class ExplorationProgressController @Inject constructor( private class ControllerState( val explorationProgress: ExplorationProgress, val isRestart: Boolean, + val isResume: Boolean, val sessionId: String, val ephemeralStateFlow: MutableStateFlow>, val commandQueue: SendChannel>, diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt index 59578d08aa2..f0fce6b94b0 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt @@ -245,6 +245,31 @@ class LearnerAnalyticsLogger @Inject constructor( getExpectedStateLogger()?.logFinishExploration() } + /** Logs that the current exploration progress has been saved successfully. */ + fun logProgressSavingSuccess() { + getExpectedStateLogger()?.logProgressSavingSuccess() + } + + /** Logs that the current exploration progress has failed to save. */ + fun logProgressSavingFailure() { + getExpectedStateLogger()?.logProgressSavingFailure() + } + + /** Logs that the user has left the lesson advertently (attempted to save). */ + fun logLessonSavedAdvertently() { + getExpectedStateLogger()?.logLessonSavedAdvertently() + } + + /** Logs that correct answer was submitted in a resumed lesson. */ + fun logResumeLessonSubmitCorrectAnswer() { + getExpectedStateLogger()?.logResumeLessonSubmitCorrectAnswer() + } + + /** Logs that incorrect answer was submitted in a resumed lesson. */ + fun logResumeLessonSubmitIncorrectAnswer() { + getExpectedStateLogger()?.logResumeLessonSubmitIncorrectAnswer() + } + /** * Begins analytics logging for the specified [newState], returning the [StateAnalyticsLogger] * that can be used to log events for the [State]. @@ -306,6 +331,31 @@ class LearnerAnalyticsLogger @Inject constructor( logStateEvent(EventBuilder::setFinishExplorationContext) } + /** Logs that the current exploration progress has been saved successfully. */ + internal fun logProgressSavingSuccess() { + logStateEvent(EventBuilder::setProgressSavingSuccessContext) + } + + /** Logs that the current exploration progress has failed to save. */ + internal fun logProgressSavingFailure() { + logStateEvent(EventBuilder::setProgressSavingFailureContext) + } + + /** Logs that the user has left the lesson advertently (attempted to save). */ + internal fun logLessonSavedAdvertently() { + logStateEvent(EventBuilder::setLessonSavedAdvertentlyContext) + } + + /** Logs that correct answer was submitted in a resumed lesson. */ + internal fun logResumeLessonSubmitCorrectAnswer() { + logStateEvent(EventBuilder::setResumeLessonSubmitCorrectAnswerContext) + } + + /** Logs that incorrect answer was submitted in a resumed lesson. */ + internal fun logResumeLessonSubmitIncorrectAnswer() { + logStateEvent(EventBuilder::setResumeLessonSubmitIncorrectAnswerContext) + } + /** Logs that this card has been started. */ fun logStartCard() { logStateEvent(linkedSkillId, ::createCardContext, EventBuilder::setStartCardContext) diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index b75654bcdf7..e133773b295 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -21,7 +21,11 @@ import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.EphemeralState.StateTypeCase.COMPLETED_STATE import org.oppia.android.app.model.EphemeralState.StateTypeCase.PENDING_STATE import org.oppia.android.app.model.EphemeralState.StateTypeCase.TERMINAL_STATE +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.HINT_UNLOCKED_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PROGRESS_SAVING_SUCCESS_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REACH_INVESTED_ENGAGEMENT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.Fraction @@ -1967,8 +1971,8 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() val exploration = loadExploration(TEST_EXPLORATION_ID_2) - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(1) + val eventLog = fakeAnalyticsEventLogger.getOldestEvent() + assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(4) assertThat(eventLog).hasStartCardContextThat { hasExplorationDetailsThat().containsTestExp2Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo(exploration.initStateName) @@ -1986,8 +1990,8 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() // Resuming shouldn't log a 'start card' event. - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(1) + val eventLog = fakeAnalyticsEventLogger.getOldestEvent() + assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(4) assertThat(eventLog).hasResumeExplorationContextThat { hasLearnerIdThat().isNotEmpty() hasInstallationIdThat().isNotEmpty() @@ -2003,8 +2007,8 @@ class ExplorationProgressControllerTest { restartExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() - val (eventLog1, eventLog2) = fakeAnalyticsEventLogger.getMostRecentEvents(count = 2) - assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(2) + val (eventLog1, eventLog2) = fakeAnalyticsEventLogger.getOldestEvents(count = 2) + assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(5) assertThat(eventLog1).hasStartOverExplorationContextThat { hasLearnerIdThat().isNotEmpty() hasInstallationIdThat().isNotEmpty() @@ -2092,7 +2096,8 @@ class ExplorationProgressControllerTest { val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT } - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + // Get the 2nd most recent event. + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvents(count = 2)[0] assertThat(hasEngagementEvent).isTrue() assertThat(eventLog).hasReachedInvestedEngagementContextThat { hasStateNameThat().isEqualTo("ItemSelectionMinOne") @@ -2158,7 +2163,8 @@ class ExplorationProgressControllerTest { val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT } - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + // Get the 2nd most recent event. + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvents(count = 2)[0] assertThat(hasEngagementEvent).isTrue() assertThat(eventLog).hasReachedInvestedEngagementContextThat { hasStateNameThat().isEqualTo("ItemSelectionMinOne") @@ -2208,7 +2214,8 @@ class ExplorationProgressControllerTest { val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT } - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + // Get the 2nd most recent event. + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvents(count = 2)[0] assertThat(hasEngagementEvent).isTrue() assertThat(eventLog).hasReachedInvestedEngagementContextThat { hasStateNameThat().isEqualTo("NumberInput") @@ -2256,6 +2263,110 @@ class ExplorationProgressControllerTest { assertThat(engagementEventCount).isEqualTo(2) } + @Test + fun testResumeExp_submitCorrectAnswer_logsResumeLessonSubmitCorrectAnswerEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + + // End, then resume the exploration and submit correct answer. + endExploration() + val checkPoint = retrieveExplorationCheckpoint(TEST_EXPLORATION_ID_2) + resumeExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2, checkPoint) + submitPrototypeState2Answer() + + // Event should be logged for correct answer submission after returning to the lesson. + val resumeLessonSubmitCorrectAnswerEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT + } + assertThat(resumeLessonSubmitCorrectAnswerEventCount).isEqualTo(1) + } + + @Test + fun testResumeExp_submitIncorrectAnswer_logsResumeLessonSubmitIncorrectAnswerEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + + // End, then resume the exploration and submit incorrect answer. + endExploration() + val checkPoint = retrieveExplorationCheckpoint(TEST_EXPLORATION_ID_2) + resumeExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2, checkPoint) + submitWrongAnswerForPrototypeState2() + + // Event should be logged for incorrect answer submission after returning to the lesson. + val resumeLessonSubmitIncorrectAnswerEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT + } + assertThat(resumeLessonSubmitIncorrectAnswerEventCount).isEqualTo(1) + } + + @Test + fun testStartOverExp_submitCorrectAnswer_doesNotLogResumeLessonSubmitCorrectAnswerEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + + // End, then start over the exploration and submit correct answer. + endExploration() + restartExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + submitPrototypeState2Answer() + + // Event should not be logged for correct answer submission after returning to the lesson. + val resumeLessonSubmitCorrectAnswerEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT + } + assertThat(resumeLessonSubmitCorrectAnswerEventCount).isEqualTo(0) + } + + @Test + fun testStartOverExp_submitIncorrectAnswer_doesNotLogResumeLessonSubmitIncorrectAnswerEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + + // End, then resume the exploration and submit incorrect answer. + endExploration() + restartExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + submitWrongAnswerForPrototypeState2() + + // Event should not be logged for incorrect answer submission after returning to the lesson. + val resumeLessonSubmitIncorrectAnswerEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT + } + assertThat(resumeLessonSubmitIncorrectAnswerEventCount).isEqualTo(0) + } + + @Test + fun testReplayExp_submitCorrectAnswer_doesNotLogResumeLessonSubmitCorrectAnswerEvent() { + logIntoAnalyticsReadyAdminProfile() + replayExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + submitPrototypeState2Answer() + + // Event should not be logged for correct answer submission after replaying lesson. + val resumeLessonSubmitCorrectAnswerEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT + } + assertThat(resumeLessonSubmitCorrectAnswerEventCount).isEqualTo(0) + } + + @Test + fun testReplayExp_submitIncorrectAnswer_doesNotLogResumeLessonSubmitIncorrectAnswerEvent() { + logIntoAnalyticsReadyAdminProfile() + replayExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + submitWrongAnswerForPrototypeState2() + + // Event should not be logged for incorrect answer submission after replaying lesson. + val resumeLessonSubmitIncorrectAnswerEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT + } + assertThat(resumeLessonSubmitIncorrectAnswerEventCount).isEqualTo(0) + } + @Test fun testSubmitAnswer_correctAnswer_logsEndCardAndSubmitAnswerEvents() { logIntoAnalyticsReadyAdminProfile() @@ -2281,7 +2392,7 @@ class ExplorationProgressControllerTest { } @Test - fun testSubmitAnswer_wrongAnswer_logsSubmitAnswerEventOnly() { + fun testSubmitAnswer_wrongAnswer_logsSubmitAnswerEvent_logsProgressSavingSuccessEvent() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() @@ -2290,17 +2401,20 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(1) - assertThat(eventLog).hasSubmitAnswerContextThat { + val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2) + assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(2) + assertThat(eventLogList[0]).hasSubmitAnswerContextThat { hasExplorationDetailsThat().containsTestExp2Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions") hasAnswerCorrectValueThat().isFalse() } + assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat { + containsTestExp2Details() + } } @Test - fun testMoveToNextState_logsStartCardEvent() { + fun testMoveToNextState_logsStartCardEvent_logsProgressSavingSuccessEvent() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() @@ -2309,17 +2423,20 @@ class ExplorationProgressControllerTest { moveToNextState() - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(1) - assertThat(eventLog).hasStartCardContextThat { + val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2) + assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(2) + assertThat(eventLogList[0]).hasStartCardContextThat { hasExplorationDetailsThat().containsTestExp2Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions") hasSkillIdThat().isEqualTo("test_skill_id_0") } + assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat { + containsTestExp2Details() + } } @Test - fun testHint_offered_logsHintOfferedEvent() { + fun testHint_offered_logsHintOfferedEvent_logsProgressSavingSuccessEvent() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() @@ -2329,16 +2446,29 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(eventLog).hasHintUnlockedContextThat { + val hintOfferedEvent = fakeAnalyticsEventLogger.getLoggedEvent { + it.context.activityContextCase == HINT_UNLOCKED_CONTEXT + }.also { + assert(it != null) + } + assertThat(hintOfferedEvent!!).hasHintUnlockedContextThat { hasExplorationDetailsThat().containsTestExp2Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions") hasHintIndexThat().isEqualTo(0) } + + val progressSavingSuccessEvent = fakeAnalyticsEventLogger.getLoggedEvent { + it.context.activityContextCase == PROGRESS_SAVING_SUCCESS_CONTEXT + }.also { + assert(it != null) + } + assertThat(progressSavingSuccessEvent!!).hasProgressSavingSuccessContextThat { + containsTestExp2Details() + } } @Test - fun testHint_offeredThenViewed_logsViewHintEvent() { + fun testHint_offeredThenViewed_logsViewHintEvent_logsProgressSavingSuccessEvent() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() @@ -2351,16 +2481,19 @@ class ExplorationProgressControllerTest { explorationProgressController.submitHintIsRevealed(hintIndex = 0) ) - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(eventLog).hasAccessHintContextThat { + val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2) + assertThat(eventLogList[0]).hasAccessHintContextThat { hasExplorationDetailsThat().containsTestExp2Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions") hasHintIndexThat().isEqualTo(0) } + assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat { + containsTestExp2Details() + } } @Test - fun testHint_lastHintWithNoSolution_offered_logsHintOfferedEvent() { + fun testHint_lastHintWithNoSolution_offered_logsHintOfferedEvent_logsProgressSavingSuccessEvt() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0) waitForGetCurrentStateSuccessfulLoad() @@ -2372,16 +2505,29 @@ class ExplorationProgressControllerTest { submitMultipleChoiceAnswer(choiceIndex = 0) submitMultipleChoiceAnswer(choiceIndex = 0) - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(eventLog).hasHintUnlockedContextThat { + val hintOfferedEvent = fakeAnalyticsEventLogger.getLoggedEvent { + it.context.activityContextCase == HINT_UNLOCKED_CONTEXT + }.also { + assert(it != null) + } + assertThat(hintOfferedEvent!!).hasHintUnlockedContextThat { hasExplorationDetailsThat().containsFractionsExp0Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Parts of a whole") hasHintIndexThat().isEqualTo(0) } + + val progressSavingSuccessEvent = fakeAnalyticsEventLogger.getLoggedEvent { + it.context.activityContextCase == PROGRESS_SAVING_SUCCESS_CONTEXT + }.also { + assert(it != null) + } + assertThat(progressSavingSuccessEvent!!).hasProgressSavingSuccessContextThat { + containsFractionsExp0Details() + } } @Test - fun testHint_lastHintWithNoSolution_offeredThenViewed_logsViewHintEvent() { + fun testHint_lastHintWithNoSol_offeredThenViewed_logsViewHintEvt_logsProgressSavingSuccessEvt() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0) waitForGetCurrentStateSuccessfulLoad() @@ -2397,16 +2543,19 @@ class ExplorationProgressControllerTest { explorationProgressController.submitHintIsRevealed(hintIndex = 0) ) - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(eventLog).hasAccessHintContextThat { + val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2) + assertThat(eventLogList[0]).hasAccessHintContextThat { hasExplorationDetailsThat().containsFractionsExp0Details() hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Parts of a whole") hasHintIndexThat().isEqualTo(0) } + assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat { + containsFractionsExp0Details() + } } @Test - fun testSolution_offered_logsSolutionOfferedEvent() { + fun testSolution_offered_logsSolutionOfferedEvent_logsProgressSavingSuccessEvent() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() @@ -2422,13 +2571,16 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(eventLog).hasSolutionUnlockedContextThat().containsTestExp2Details() - assertThat(eventLog).hasSolutionUnlockedContextThat().hasStateNameThat().isEqualTo("Fractions") + val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2) + assertThat(eventLogList[0]).hasSolutionUnlockedContextThat { + containsTestExp2Details() + hasStateNameThat().isEqualTo("Fractions") + } + assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat().containsTestExp2Details() } @Test - fun testSolution_offeredThenViewed_logsViewSolutionEvent() { + fun testSolution_offeredThenViewed_logsViewSolutionEvent_logsProgressSavingSuccessEvent() { logIntoAnalyticsReadyAdminProfile() startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) waitForGetCurrentStateSuccessfulLoad() @@ -2447,9 +2599,12 @@ class ExplorationProgressControllerTest { explorationProgressController.submitSolutionIsRevealed() ) - val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() - assertThat(eventLog).hasAccessSolutionContextThat().containsTestExp2Details() - assertThat(eventLog).hasAccessSolutionContextThat().hasStateNameThat().isEqualTo("Fractions") + val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2) + assertThat(eventLogList[0]).hasAccessSolutionContextThat { + containsTestExp2Details() + hasStateNameThat().isEqualTo("Fractions") + } + assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat().containsTestExp2Details() } @Test diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt index 80368c18df9..b477b24bd75 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt @@ -1786,6 +1786,131 @@ class LearnerAnalyticsLoggerTest { } } + @Test + fun testExpLogger_logProgressSavingSuccess_logsEventWithContext() { + val exploration5 = loadExploration(TEST_EXPLORATION_ID_5) + val expLogger = learnerAnalyticsLogger.beginExploration(exploration5) + testCoroutineDispatchers.runCurrent() + + expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME)) + expLogger.logProgressSavingSuccess() + testCoroutineDispatchers.runCurrent() + + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(eventLog).hasProgressSavingSuccessContextThat { + hasTopicIdThat().isEqualTo(TEST_TOPIC_ID) + hasStoryIdThat().isEqualTo(TEST_STORY_ID) + hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5) + hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID) + hasVersionThat().isEqualTo(5) + hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME) + hasLearnerDetailsThat { + hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID) + hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID) + } + } + } + + @Test + fun testExpLogger_logProgressSavingFailure_logsEventWithContext() { + val exploration5 = loadExploration(TEST_EXPLORATION_ID_5) + val expLogger = learnerAnalyticsLogger.beginExploration(exploration5) + testCoroutineDispatchers.runCurrent() + + expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME)) + expLogger.logProgressSavingFailure() + testCoroutineDispatchers.runCurrent() + + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(eventLog).hasProgressSavingFailureContextThat { + hasTopicIdThat().isEqualTo(TEST_TOPIC_ID) + hasStoryIdThat().isEqualTo(TEST_STORY_ID) + hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5) + hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID) + hasVersionThat().isEqualTo(5) + hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME) + hasLearnerDetailsThat { + hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID) + hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID) + } + } + } + + @Test + fun testExpLogger_logLessonSavedAdvertently_logsEventWithContext() { + val exploration5 = loadExploration(TEST_EXPLORATION_ID_5) + val expLogger = learnerAnalyticsLogger.beginExploration(exploration5) + testCoroutineDispatchers.runCurrent() + + expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME)) + expLogger.logLessonSavedAdvertently() + testCoroutineDispatchers.runCurrent() + + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(eventLog).hasLessonSavedAdvertentlyContextThat { + hasTopicIdThat().isEqualTo(TEST_TOPIC_ID) + hasStoryIdThat().isEqualTo(TEST_STORY_ID) + hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5) + hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID) + hasVersionThat().isEqualTo(5) + hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME) + hasLearnerDetailsThat { + hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID) + hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID) + } + } + } + + @Test + fun testExpLogger_logResumeLessonSubmitCorrectAnswer_logsEventWithContext() { + val exploration5 = loadExploration(TEST_EXPLORATION_ID_5) + val expLogger = learnerAnalyticsLogger.beginExploration(exploration5) + testCoroutineDispatchers.runCurrent() + + expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME)) + expLogger.logResumeLessonSubmitCorrectAnswer() + testCoroutineDispatchers.runCurrent() + + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(eventLog).hasResumeLessonSubmitCorrectAnswerContextThat { + hasTopicIdThat().isEqualTo(TEST_TOPIC_ID) + hasStoryIdThat().isEqualTo(TEST_STORY_ID) + hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5) + hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID) + hasVersionThat().isEqualTo(5) + hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME) + hasLearnerDetailsThat { + hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID) + hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID) + } + } + } + + @Test + fun testExpLogger_logResumeLessonSubmitIncorrectAnswer_logsEventWithContext() { + val exploration5 = loadExploration(TEST_EXPLORATION_ID_5) + val expLogger = learnerAnalyticsLogger.beginExploration(exploration5) + testCoroutineDispatchers.runCurrent() + + expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME)) + expLogger.logResumeLessonSubmitIncorrectAnswer() + testCoroutineDispatchers.runCurrent() + + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(eventLog).hasResumeLessonSubmitIncorrectAnswerContextThat { + hasTopicIdThat().isEqualTo(TEST_TOPIC_ID) + hasStoryIdThat().isEqualTo(TEST_STORY_ID) + hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5) + hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID) + hasVersionThat().isEqualTo(5) + hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME) + hasLearnerDetailsThat { + hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID) + hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID) + } + } + } + private fun loadExploration(expId: String): Exploration { return monitorFactory.waitForNextSuccessfulResult( explorationDataController.getExplorationById(profileId, expId) diff --git a/model/src/main/proto/oppia_logger.proto b/model/src/main/proto/oppia_logger.proto index c1bfa9f65dd..74b429f9d8b 100644 --- a/model/src/main/proto/oppia_logger.proto +++ b/model/src/main/proto/oppia_logger.proto @@ -191,6 +191,24 @@ message EventLog { // The event being logged is related to the amount spent by the user with the app in // foreground. AppInForegroundTimeContext app_in_foreground_time = 48; + + // The event being logged is related to lesson progress(checkpoint) saving success. + ExplorationContext progress_saving_success_context = 49; + + // The event being logged is related to lesson progress(checkpoint) saving failure. + ExplorationContext progress_saving_failure_context = 50; + + // The event being logged is related to lesson progress(checkpoint) saving attempt + // advertently(intentionally). + ExplorationContext lesson_saved_advertently_context = 51; + + // The event being logged is related to correct answer submission after returning back to the + // lesson. + ExplorationContext resume_lesson_submit_correct_answer_context = 52; + + // The event being logged is related to incorrect answer submission after returning back to + // the lesson. + ExplorationContext resume_lesson_submit_incorrect_answer_context = 53; } } diff --git a/testing/src/main/java/org/oppia/android/testing/FakeAnalyticsEventLogger.kt b/testing/src/main/java/org/oppia/android/testing/FakeAnalyticsEventLogger.kt index 61721ae788d..ef425464ca3 100644 --- a/testing/src/main/java/org/oppia/android/testing/FakeAnalyticsEventLogger.kt +++ b/testing/src/main/java/org/oppia/android/testing/FakeAnalyticsEventLogger.kt @@ -18,6 +18,9 @@ class FakeAnalyticsEventLogger @Inject constructor() : AnalyticsEventLogger { /** Returns the oldest event that's been logged. */ fun getOldestEvent(): EventLog = eventList.first() + /** Returns the most oldest [count] logged events. */ + fun getOldestEvents(count: Int): List = eventList.take(count) + /** Returns the most recently logged event. */ fun getMostRecentEvent(): EventLog = getMostRecentEvents(count = 1).first() @@ -30,6 +33,9 @@ class FakeAnalyticsEventLogger @Inject constructor() : AnalyticsEventLogger { /** Returns whether a certain event has been logged or not, based on the provided [predicate]. */ fun hasEventLogged(predicate: (EventLog) -> Boolean): Boolean = eventList.find(predicate) != null + /** Returns a certain event if it has been logged or null, based on the provided [predicate]. */ + fun getLoggedEvent(predicate: (EventLog) -> Boolean): EventLog? = eventList.find(predicate) + /** Returns the number of logged events that match the provided [predicate]. */ fun countEvents(predicate: (EventLog) -> Boolean): Int = eventList.count(predicate) diff --git a/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt b/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt index abec71d8ec0..ad3e89f662e 100644 --- a/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt @@ -28,6 +28,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.EXIT_EXP import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.FINISH_EXPLORATION_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.HINT_UNLOCKED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.INSTALL_ID_FOR_FAILED_ANALYTICS_LOG +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.LESSON_SAVED_ADVERTENTLY_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.MANDATORY_RESPONSE import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_CONCEPT_CARD import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_EXPLORATION_ACTIVITY @@ -43,8 +44,12 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STO import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPTIONAL_RESPONSE import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PROGRESS_SAVING_FAILURE_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PROGRESS_SAVING_SUCCESS_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REACH_INVESTED_ENGAGEMENT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_EXPLORATION_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SHOW_SURVEY_POPUP import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT @@ -1082,6 +1087,142 @@ class EventLogSubject private constructor( hasOptionalSurveyResponseContextThat().block() } + /** + * Verifies that the [EventLog] under test has a context corresponding to + * [PROGRESS_SAVING_SUCCESS_CONTEXT] (per [EventLog.Context.getActivityContextCase]). + */ + fun hasProgressSavingSuccessContext() { + assertThat(actual.context.activityContextCase).isEqualTo(PROGRESS_SAVING_SUCCESS_CONTEXT) + } + + /** + * Verifies the [EventLog]'s context per [hasProgressSavingSuccessContext] and returns a + * [ExplorationContextSubject] to test the corresponding context. + */ + fun hasProgressSavingSuccessContextThat(): ExplorationContextSubject { + hasProgressSavingSuccessContext() + return ExplorationContextSubject.assertThat(actual.context.progressSavingSuccessContext) + } + + /** + * Verifies the [EventLog]'s context and executes [block] in the same way as + * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, + * [hasProgressSavingSuccessContextThat]. + */ + fun hasProgressSavingSuccessContextThat(block: ExplorationContextSubject.() -> Unit) { + hasProgressSavingSuccessContextThat().block() + } + + /** + * Verifies that the [EventLog] under test has a context corresponding to + * [PROGRESS_SAVING_FAILURE_CONTEXT] (per [EventLog.Context.getActivityContextCase]). + */ + fun hasProgressSavingFailureContext() { + assertThat(actual.context.activityContextCase).isEqualTo(PROGRESS_SAVING_FAILURE_CONTEXT) + } + + /** + * Verifies the [EventLog]'s context per [hasProgressSavingFailureContext] and returns a + * [ExplorationContextSubject] to test the corresponding context. + */ + fun hasProgressSavingFailureContextThat(): ExplorationContextSubject { + hasProgressSavingFailureContext() + return ExplorationContextSubject.assertThat(actual.context.progressSavingFailureContext) + } + + /** + * Verifies the [EventLog]'s context and executes [block] in the same way as + * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, + * [hasProgressSavingFailureContextThat]. + */ + fun hasProgressSavingFailureContextThat(block: ExplorationContextSubject.() -> Unit) { + hasProgressSavingFailureContextThat().block() + } + + /** + * Verifies that the [EventLog] under test has a context corresponding to + * [LESSON_SAVED_ADVERTENTLY_CONTEXT] (per [EventLog.Context.getActivityContextCase]). + */ + fun hasLessonSavedAdvertentlyContext() { + assertThat(actual.context.activityContextCase).isEqualTo(LESSON_SAVED_ADVERTENTLY_CONTEXT) + } + + /** + * Verifies the [EventLog]'s context per [hasLessonSavedAdvertentlyContext] and returns a + * [ExplorationContextSubject] to test the corresponding context. + */ + fun hasLessonSavedAdvertentlyContextThat(): ExplorationContextSubject { + hasLessonSavedAdvertentlyContext() + return ExplorationContextSubject.assertThat(actual.context.lessonSavedAdvertentlyContext) + } + + /** + * Verifies the [EventLog]'s context and executes [block] in the same way as + * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, + * [hasLessonSavedAdvertentlyContextThat]. + */ + fun hasLessonSavedAdvertentlyContextThat(block: ExplorationContextSubject.() -> Unit) { + hasLessonSavedAdvertentlyContextThat().block() + } + + /** + * Verifies that the [EventLog] under test has a context corresponding to + * [RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT] (per [EventLog.Context.getActivityContextCase]). + */ + fun hasResumeLessonSubmitCorrectAnswerContext() { + assertThat(actual.context.activityContextCase) + .isEqualTo(RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT) + } + + /** + * Verifies the [EventLog]'s context per [hasResumeLessonSubmitCorrectAnswerContext] and returns + * a [ExplorationContextSubject] to test the corresponding context. + */ + fun hasResumeLessonSubmitCorrectAnswerContextThat(): ExplorationContextSubject { + hasResumeLessonSubmitCorrectAnswerContext() + return ExplorationContextSubject.assertThat( + actual.context.resumeLessonSubmitCorrectAnswerContext + ) + } + + /** + * Verifies the [EventLog]'s context and executes [block] in the same way as + * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, + * [hasResumeLessonSubmitCorrectAnswerContextThat]. + */ + fun hasResumeLessonSubmitCorrectAnswerContextThat(block: ExplorationContextSubject.() -> Unit) { + hasResumeLessonSubmitCorrectAnswerContextThat().block() + } + + /** + * Verifies that the [EventLog] under test has a context corresponding to + * [RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT] (per [EventLog.Context.getActivityContextCase]). + */ + fun hasResumeLessonSubmitIncorrectAnswerContext() { + assertThat(actual.context.activityContextCase) + .isEqualTo(RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT) + } + + /** + * Verifies the [EventLog]'s context per [hasResumeLessonSubmitIncorrectAnswerContext] and + * returns a [ExplorationContextSubject] to test the corresponding context. + */ + fun hasResumeLessonSubmitIncorrectAnswerContextThat(): ExplorationContextSubject { + hasResumeLessonSubmitIncorrectAnswerContext() + return ExplorationContextSubject.assertThat( + actual.context.resumeLessonSubmitIncorrectAnswerContext + ) + } + + /** + * Verifies the [EventLog]'s context and executes [block] in the same way as + * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, + * [hasResumeLessonSubmitIncorrectAnswerContextThat]. + */ + fun hasResumeLessonSubmitIncorrectAnswerContextThat(block: ExplorationContextSubject.() -> Unit) { + hasResumeLessonSubmitIncorrectAnswerContextThat().block() + } + /** * Truth subject for verifying properties of [AppLanguageSelection]s. * diff --git a/testing/src/test/java/org/oppia/android/testing/FakeAnalyticsEventLoggerTest.kt b/testing/src/test/java/org/oppia/android/testing/FakeAnalyticsEventLoggerTest.kt index b6b3870763a..f165269bcad 100644 --- a/testing/src/test/java/org/oppia/android/testing/FakeAnalyticsEventLoggerTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/FakeAnalyticsEventLoggerTest.kt @@ -261,6 +261,105 @@ class FakeAnalyticsEventLoggerTest { assertThat(mostRecentEvents).containsExactly(eventLog2) } + @Test + fun testOldestEvents_twoEvents_noEventsLogged_returnsEmptyList() { + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 2) + + assertThat(oldestEvents).isEmpty() + } + + @Test + fun testOldestEvents_twoEvents_oneEventLogged_returnsOneItemList() { + analyticsEventLogger.logEvent(eventLog1) + + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 2) + + assertThat(oldestEvents).containsExactly(eventLog1) + } + + @Test + fun testOldestEvents_twoEvents_twoEventsLogged_returnsEventsInOrder() { + analyticsEventLogger.logEvent(eventLog2) + analyticsEventLogger.logEvent(eventLog1) + + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 2) + + assertThat(oldestEvents).containsExactly(eventLog2, eventLog1).inOrder() + } + + @Test + fun testOldestEvents_oneEvent_twoEventsLogged_returnsSingleOldestEvent() { + analyticsEventLogger.logEvent(eventLog2) + analyticsEventLogger.logEvent(eventLog1) + + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 1) + + assertThat(oldestEvents).containsExactly(eventLog2) + } + + @Test + fun testOldestEvents_zeroEvents_twoEventsLogged_returnsEmptyList() { + analyticsEventLogger.logEvent(eventLog2) + analyticsEventLogger.logEvent(eventLog1) + + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 0) + + assertThat(oldestEvents).isEmpty() + } + + @Test + fun testOldestEvents_negativeEvents_twoEventsLogged_throwsException() { + analyticsEventLogger.logEvent(eventLog2) + analyticsEventLogger.logEvent(eventLog1) + + assertThrows() { + fakeAnalyticsEventLogger.getOldestEvents(count = -1) + } + } + + @Test + fun testOldestEvents_twoEventsLogged_eventsCleared_returnsEmptyList() { + analyticsEventLogger.logEvent(eventLog2) + analyticsEventLogger.logEvent(eventLog1) + fakeAnalyticsEventLogger.clearAllEvents() + + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 2) + + assertThat(oldestEvents).isEmpty() + } + + @Test + fun testOldestEvents_eventLogged_cleared_newEventLogged_returnsNewestEvent() { + analyticsEventLogger.logEvent(eventLog1) + fakeAnalyticsEventLogger.clearAllEvents() + analyticsEventLogger.logEvent(eventLog2) + + val oldestEvents = fakeAnalyticsEventLogger.getOldestEvents(count = 2) + + assertThat(oldestEvents).containsExactly(eventLog2) + } + + @Test + fun testLoggedEvent_eventsLogged_getLoggedEvent_returnsLoggedEventCorrectly() { + analyticsEventLogger.logEvent(eventLog1) + analyticsEventLogger.logEvent(eventLog2) + + val loggedEvent = fakeAnalyticsEventLogger.getLoggedEvent { + it.priority == Priority.ESSENTIAL + } + + assertThat(loggedEvent).isEqualTo(eventLog1) + } + + @Test + fun testLoggedEvent_noEventsLogged_getLoggedEvent_returnsNull() { + val loggedEvent = fakeAnalyticsEventLogger.getLoggedEvent { + it.priority == Priority.ESSENTIAL + } + + assertThat(loggedEvent).isNull() + } + private fun setUpTestApplicationComponent() { DaggerFakeAnalyticsEventLoggerTest_TestApplicationComponent.builder() .setApplication(ApplicationProvider.getApplicationContext()) diff --git a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt index d3d177d27a6..2b5ad794f74 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt @@ -24,6 +24,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.EXIT_EXP import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.FINISH_EXPLORATION_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.HINT_UNLOCKED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.INSTALL_ID_FOR_FAILED_ANALYTICS_LOG +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.LESSON_SAVED_ADVERTENTLY_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.MANDATORY_RESPONSE import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_CONCEPT_CARD import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_EXPLORATION_ACTIVITY @@ -39,8 +40,12 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STO import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPTIONAL_RESPONSE import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PROGRESS_SAVING_FAILURE_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PROGRESS_SAVING_SUCCESS_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REACH_INVESTED_ENGAGEMENT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_EXPLORATION_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT +import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RETROFIT_CALL_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RETROFIT_CALL_FAILED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SHOW_SURVEY_POPUP @@ -231,6 +236,16 @@ class EventBundleCreator @Inject constructor( APP_IN_FOREGROUND_CONTEXT -> LearnerDetailsContext(activityName, appInForegroundContext) EXIT_EXPLORATION_CONTEXT -> ExplorationContext(activityName, exitExplorationContext) FINISH_EXPLORATION_CONTEXT -> ExplorationContext(activityName, finishExplorationContext) + PROGRESS_SAVING_SUCCESS_CONTEXT -> + ExplorationContext(activityName, progressSavingSuccessContext) + PROGRESS_SAVING_FAILURE_CONTEXT -> + ExplorationContext(activityName, progressSavingFailureContext) + LESSON_SAVED_ADVERTENTLY_CONTEXT -> + ExplorationContext(activityName, lessonSavedAdvertentlyContext) + RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT -> + ExplorationContext(activityName, resumeLessonSubmitCorrectAnswerContext) + RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT -> + ExplorationContext(activityName, resumeLessonSubmitIncorrectAnswerContext) RESUME_EXPLORATION_CONTEXT -> LearnerDetailsContext(activityName, resumeExplorationContext) START_OVER_EXPLORATION_CONTEXT -> LearnerDetailsContext(activityName, startOverExplorationContext) diff --git a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt index 726eb02c85e..bfb1e9dc71c 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt @@ -38,6 +38,13 @@ class KenyaAlphaEventTypeToHumanReadableNameConverterImpl @Inject constructor() ActivityContextCase.APP_IN_FOREGROUND_CONTEXT -> "app_in_foreground_context" ActivityContextCase.EXIT_EXPLORATION_CONTEXT -> "exit_exploration_context" ActivityContextCase.FINISH_EXPLORATION_CONTEXT -> "finish_exploration_context" + ActivityContextCase.PROGRESS_SAVING_SUCCESS_CONTEXT -> "progress_saving_success_context" + ActivityContextCase.PROGRESS_SAVING_FAILURE_CONTEXT -> "progress_saving_failure_context" + ActivityContextCase.LESSON_SAVED_ADVERTENTLY_CONTEXT -> "lesson_saved_advertently_context" + ActivityContextCase.RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT -> + "resume_lesson_submit_correct_answer_context" + ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT -> + "resume_lesson_submit_incorrect_answer_context" ActivityContextCase.RESUME_EXPLORATION_CONTEXT -> "resume_exploration_context" ActivityContextCase.START_OVER_EXPLORATION_CONTEXT -> "start_over_exploration_context" ActivityContextCase.DELETE_PROFILE_CONTEXT -> "delete_profile_context" diff --git a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt index 565ee460ca1..4673e2c6382 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt @@ -48,6 +48,13 @@ class StandardEventTypeToHumanReadableNameConverterImpl @Inject constructor() : ActivityContextCase.APP_IN_FOREGROUND_CONTEXT -> "bring_app_to_foreground" ActivityContextCase.EXIT_EXPLORATION_CONTEXT -> "leave_exploration" ActivityContextCase.FINISH_EXPLORATION_CONTEXT -> "complete_exploration" + ActivityContextCase.PROGRESS_SAVING_SUCCESS_CONTEXT -> "progress_saving_success" + ActivityContextCase.PROGRESS_SAVING_FAILURE_CONTEXT -> "progress_saving_failure" + ActivityContextCase.LESSON_SAVED_ADVERTENTLY_CONTEXT -> "lesson_saved_advertently" + ActivityContextCase.RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT -> + "submit_correct_ans_in_resumed_lesson" + ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT -> + "submit_incorrect_ans_in_resumed_lesson" ActivityContextCase.RESUME_EXPLORATION_CONTEXT -> "resume_in_progress_exploration" ActivityContextCase.START_OVER_EXPLORATION_CONTEXT -> "restart_in_progress_exploration" ActivityContextCase.DELETE_PROFILE_CONTEXT -> "delete_profile"