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"