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 d6971cbcf11..7f6e9f7c56f 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 @@ -1046,6 +1046,9 @@ class ExplorationProgressController @Inject constructor( private var helpIndex = HelpIndex.getDefaultInstance() private var availableCardCount: Int = -1 + private var hasReachedInvestedEngagement = false + private var completedStateCount = 0 + /** * The [LearnerAnalyticsLogger.ExplorationAnalyticsLogger] to be used for logging * exploration-specific events. @@ -1087,6 +1090,13 @@ class ExplorationProgressController @Inject constructor( // Force the card count to update. availableCardCount = explorationProgress.stateDeck.getViewedStateCount() + + if (!hasReachedInvestedEngagement && + completedStateCount >= MINIMUM_COMPLETED_STATE_COUNT_FOR_INVESTED_ENGAGEMENT + ) { + it.logInvestedEngagement() + hasReachedInvestedEngagement = true + } } } @@ -1106,6 +1116,7 @@ class ExplorationProgressController @Inject constructor( fun endState() { stateAnalyticsLogger?.logEndCard() explorationAnalyticsLogger.endCard() + completedStateCount++ } /** Checks and logs for hint-based changes based on the provided [HelpIndex]. */ @@ -1279,6 +1290,8 @@ class ExplorationProgressController @Inject constructor( } private companion object { + private const val MINIMUM_COMPLETED_STATE_COUNT_FOR_INVESTED_ENGAGEMENT = 3 + /** * Returns a collectable [Flow] that notifies [collector] for this [StateFlow]s initial state, * and every change after. 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 cf3990c544d..727b53dc722 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 @@ -384,6 +384,15 @@ class LearnerAnalyticsLogger @Inject constructor( logStateEvent(contentId, ::createPlayVoiceOverContext, EventBuilder::setPlayVoiceOverContext) } + /** + * Logs that the learner has demonstrated an invested engagement in the lesson (that is, they've + * played far enough in the lesson to indicate that they're not just quickly browsing & then + * leaving). + */ + fun logInvestedEngagement() { + logStateEvent(EventBuilder::setReachInvestedEngagement) + } + private fun logStateEvent(setter: EventBuilder.(ExplorationContext) -> EventBuilder) = logStateEvent(Unit, { _, context -> context }, setter) 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 ea1f23e0203..e48e104ccd1 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,6 +21,7 @@ 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.REACH_INVESTED_ENGAGEMENT import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.Fraction @@ -1972,6 +1973,226 @@ class ExplorationProgressControllerTest { } } + @Test + fun testPlayNewExp_firstCard_notFinished_doesNotLogReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(hasEngagementEvent).isFalse() + } + + @Test + fun testPlayNewExp_finishFirstCard_moveToSecond_doesNotLogReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(hasEngagementEvent).isFalse() + } + + @Test + fun testPlayNewExp_finishThreeCards_doNotProceed_doesNotLogReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + submitPrototypeState3Answer() + + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(hasEngagementEvent).isFalse() + } + + @Test + fun testPlayNewExp_finishThreeCards_moveToFour_logsReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + playThroughPrototypeState3AndMoveToNextState() + + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(hasEngagementEvent).isTrue() + assertThat(eventLog).hasReachedInvestedEngagementContextThat { + hasStateNameThat().isEqualTo("ItemSelectionMinOne") + } + } + + @Test + fun testPlayNewExp_finishFourCards_moveToFive_logsReachInvestedEngagementEventOnlyOnce() { + logIntoAnalyticsReadyAdminProfile() + + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + playThroughPrototypeState3AndMoveToNextState() + playThroughPrototypeState4AndMoveToNextState() + + // The engagement event should only be logged once during a play session, even if the user + // continues past that point. + val engagementEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(engagementEventCount).isEqualTo(1) + } + + @Test + fun testPlayNewExp_firstTwo_startOver_playFirst_doesNotLogReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + + // Restart the exploration. + restartExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + + // No engagement event should be logged, even though 3 total states were completed from the + // first and second sessions (cumulatively). + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(hasEngagementEvent).isFalse() + } + + @Test + fun testPlayNewExp_firstTwo_startOver_playThreeAndMove_logsReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + + // Restart the exploration. + restartExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + playThroughPrototypeState3AndMoveToNextState() + + // An engagement event should be logged since the new session uniquely finished 3 states. + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(hasEngagementEvent).isTrue() + assertThat(eventLog).hasReachedInvestedEngagementContextThat { + hasStateNameThat().isEqualTo("ItemSelectionMinOne") + } + } + + @Test + fun testResumeExp_stateOneTwoDone_finishThreeAndMoveForward_noLogReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + + // End, then resume the exploration and complete the third state. + endExploration() + val checkPoint = retrieveExplorationCheckpoint(TEST_EXPLORATION_ID_2) + resumeExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2, checkPoint) + playThroughPrototypeState3AndMoveToNextState() + + // Despite the first three states now being completed, this isn't an engagement event since the + // user hasn't finished three states within *one* session. + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(hasEngagementEvent).isFalse() + } + + @Test + fun testResumeExp_stateOneTwoDone_finishThreeMoreAndMove_logsReachInvestedEngagementEvent() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + + // End, then resume the exploration and complete the third state. + endExploration() + val checkPoint = retrieveExplorationCheckpoint(TEST_EXPLORATION_ID_2) + resumeExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2, checkPoint) + playThroughPrototypeState3AndMoveToNextState() + playThroughPrototypeState4AndMoveToNextState() + playThroughPrototypeState5AndMoveToNextState() + + // An engagement event should be logged now since the user completed 3 new states in the current + // session. + val hasEngagementEvent = fakeAnalyticsEventLogger.hasEventLogged { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(hasEngagementEvent).isTrue() + assertThat(eventLog).hasReachedInvestedEngagementContextThat { + hasStateNameThat().isEqualTo("NumberInput") + } + } + + @Test + fun testResumeExp_finishThree_thenAnotherThreeAfterResume_logsInvestedEngagementEventTwice() { + logIntoAnalyticsReadyAdminProfile() + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + waitForGetCurrentStateSuccessfulLoad() + playThroughPrototypeState1AndMoveToNextState() + playThroughPrototypeState2AndMoveToNextState() + playThroughPrototypeState3AndMoveToNextState() + + // End, then resume the exploration and complete the third state. + endExploration() + val checkPoint = retrieveExplorationCheckpoint(TEST_EXPLORATION_ID_2) + resumeExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2, checkPoint) + playThroughPrototypeState4AndMoveToNextState() + playThroughPrototypeState5AndMoveToNextState() + playThroughPrototypeState6AndMoveToNextState() + + // Playing enough states for the engagement event before and after resuming should result in it + // being logged twice (once for each session). + val engagementEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(engagementEventCount).isEqualTo(2) + } + + @Test + fun testPlayNewExp_getToEngagementEvent_playOtherExpAndDoSame_logsEngagementEventAgain() { + logIntoAnalyticsReadyAdminProfile() + + // Play through the full prototype exploration twice. + playThroughPrototypeExplorationInNewSession() + playThroughPrototypeExplorationInNewSession() + + // Playing through two complete exploration sessions should result in the engagement event being + // logged twice (once for each session). + val engagementEventCount = fakeAnalyticsEventLogger.countEvents { + it.context.activityContextCase == REACH_INVESTED_ENGAGEMENT + } + assertThat(engagementEventCount).isEqualTo(2) + } + @Test fun testSubmitAnswer_correctAnswer_logsEndCardAndSubmitAnswerEvents() { logIntoAnalyticsReadyAdminProfile() @@ -2325,6 +2546,12 @@ class ExplorationProgressControllerTest { return waitForGetCurrentStateSuccessfulLoad() } + private fun playThroughPrototypeExplorationInNewSession() { + startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2) + playThroughPrototypeExploration() + endExploration() + } + private fun playThroughPrototypeExploration(): EphemeralState { playThroughPrototypeState1AndMoveToNextState() playThroughPrototypeState2AndMoveToNextState() 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 6c4bab590da..1b950ebe901 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 @@ -1400,6 +1400,30 @@ class LearnerAnalyticsLoggerTest { assertThat(log.type).isEqualTo(Log.ERROR) } + @Test + fun testStateAnalyticsLogger_logReachInvestedEngagement_logsStateEventWithStateName() { + val exploration5 = loadExploration(TEST_EXPLORATION_ID_5) + val expLogger = learnerAnalyticsLogger.beginExploration(exploration5) + val stateLogger = expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME)) + + stateLogger.logInvestedEngagement() + + val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent() + assertThat(eventLog).isEssentialPriority() + assertThat(eventLog).hasReachedInvestedEngagementContextThat { + 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 32c2290ffc2..6f2a37fdd2f 100644 --- a/model/src/main/proto/oppia_logger.proto +++ b/model/src/main/proto/oppia_logger.proto @@ -123,6 +123,10 @@ message EventLog { // value here has no importance and is always 'true'. bool open_profile_chooser = 32; + // The event being logged indicates that the user has reached an invested level of learning + // engagement in a lesson. + ExplorationContext reach_invested_engagement = 34; + // Indicates that something went wrong when trying to log a learner analytics even for the // device corresponding to the specified device ID. string install_id_for_failed_analytics_log = 33; 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 80fdd7fc8c7..0d6a666d204 100644 --- a/testing/src/main/java/org/oppia/android/testing/FakeAnalyticsEventLogger.kt +++ b/testing/src/main/java/org/oppia/android/testing/FakeAnalyticsEventLogger.kt @@ -27,8 +27,11 @@ class FakeAnalyticsEventLogger @Inject constructor() : AnalyticsEventLogger { /** Clears all the events that are currently logged. */ fun clearAllEvents() = eventList.clear() - /** Checks if a certain event has been logged or not. */ - fun hasEventLogged(eventLog: EventLog): Boolean = eventList.contains(eventLog) + /** 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 the number of logged events that match the provided [predicate]. */ + fun countEvents(predicate: (EventLog) -> Boolean): Int = eventList.count(predicate) /** Returns true if there are no events logged. */ fun noEventsPresent(): Boolean = eventList.isEmpty() 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 a36ee70eb06..5d57cf754f1 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 @@ -32,6 +32,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REV import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REVISION_TAB import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STORY_ACTIVITY import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_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.SOLUTION_OFFERED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT @@ -143,7 +144,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenInfoTabContextThat]. */ @@ -169,7 +170,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenLessonsTabContextThat]. */ @@ -195,7 +196,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenPracticeTabContextThat]. */ @@ -221,7 +222,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenRevisionTabContextThat]. */ @@ -247,7 +248,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenQuestionPlayerContextThat]. */ @@ -273,7 +274,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenStoryActivityContextThat]. */ @@ -299,7 +300,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenConceptCardContextThat]. */ @@ -325,7 +326,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasOpenRevisionCardContextThat]. */ @@ -351,7 +352,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasStartCardContextThat]. */ @@ -377,7 +378,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasEndCardContextThat]. */ @@ -403,7 +404,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasHintOfferedContextThat]. */ @@ -429,7 +430,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasAccessHintContextThat]. */ @@ -455,7 +456,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasSolutionOfferedContextThat]. */ @@ -481,7 +482,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasAccessSolutionContextThat]. */ @@ -507,7 +508,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasSubmitAnswerContextThat]. */ @@ -533,7 +534,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasPlayVoiceOverContextThat]. */ @@ -559,7 +560,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasAppInBackgroundContextThat]. */ @@ -585,7 +586,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasAppInForegroundContextThat]. */ @@ -611,7 +612,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasExitExplorationContextThat]. */ @@ -637,7 +638,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasFinishExplorationContextThat]. */ @@ -663,7 +664,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasResumeExplorationContextThat]. */ @@ -689,7 +690,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasStartOverExplorationContextThat]. */ @@ -715,7 +716,7 @@ class EventLogSubject private constructor( } /** - * Verifies the [EventLog]'s context and executes [block] in the same was as + * Verifies the [EventLog]'s context and executes [block] in the same way as * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by, * [hasDeleteProfileContextThat]. */ @@ -757,6 +758,32 @@ class EventLogSubject private constructor( return assertThat(actual.context.openProfileChooser) } + /** + * Verifies that the [EventLog] under test has a context corresponding to + * [REACH_INVESTED_ENGAGEMENT] (per [EventLog.Context.getActivityContextCase]). + */ + fun hasReachedInvestedEngagementContext() { + assertThat(actual.context.activityContextCase).isEqualTo(REACH_INVESTED_ENGAGEMENT) + } + + /** + * Verifies the [EventLog]'s context per [hasReachedInvestedEngagementContext] and returns a + * [ExplorationContextSubject] to test the corresponding context. + */ + fun hasReachedInvestedEngagementContextThat(): ExplorationContextSubject { + hasReachedInvestedEngagementContext() + return ExplorationContextSubject.assertThat(actual.context.reachInvestedEngagement) + } + + /** + * Verifies the [EventLog]'s context and executes [block] in the same way as + * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned + * by, [hasReachedInvestedEngagementContextThat]. + */ + fun hasReachedInvestedEngagementContextThat(block: ExplorationContextSubject.() -> Unit) { + hasReachedInvestedEngagementContextThat().block() + } + /** * Verifies that the [EventLog] under test has a context corresponding to * [INSTALL_ID_FOR_FAILED_ANALYTICS_LOG] (per [EventLog.Context.getActivityContextCase]). 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 46006c807ba..c3d4b258494 100644 --- a/testing/src/test/java/org/oppia/android/testing/FakeAnalyticsEventLoggerTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/FakeAnalyticsEventLoggerTest.kt @@ -130,8 +130,8 @@ class FakeAnalyticsEventLoggerTest { analyticsEventLogger.logEvent(eventLog1) analyticsEventLogger.logEvent(eventLog2) - val eventLogStatus1 = fakeAnalyticsEventLogger.hasEventLogged(eventLog1) - val eventLogStatus2 = fakeAnalyticsEventLogger.hasEventLogged(eventLog2) + val eventLogStatus1 = fakeAnalyticsEventLogger.hasEventLogged { it == eventLog1 } + val eventLogStatus2 = fakeAnalyticsEventLogger.hasEventLogged { it == eventLog2 } val eventListStatus = fakeAnalyticsEventLogger.noEventsPresent() assertThat(eventListStatus).isFalse() 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 1b2d9876fd8..db860e80ed0 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 @@ -27,6 +27,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REV import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REVISION_TAB import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STORY_ACTIVITY import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_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.SOLUTION_OFFERED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT @@ -174,6 +175,7 @@ class EventBundleCreator @Inject constructor( DELETE_PROFILE_CONTEXT -> LearnerDetailsContext(activityName, deleteProfileContext) OPEN_HOME -> EmptyContext(activityName) OPEN_PROFILE_CHOOSER -> EmptyContext(activityName) + REACH_INVESTED_ENGAGEMENT -> ExplorationContext(activityName, reachInvestedEngagement) INSTALL_ID_FOR_FAILED_ANALYTICS_LOG -> SensitiveStringContext(activityName, installIdForFailedAnalyticsLog, "install_id") ACTIVITYCONTEXT_NOT_SET, null -> EmptyContext(activityName) // No context to create here. 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 d420258d726..9c5fd669585 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 @@ -41,6 +41,7 @@ class KenyaAlphaEventTypeToHumanReadableNameConverterImpl @Inject constructor() ActivityContextCase.DELETE_PROFILE_CONTEXT -> "delete_profile_context" ActivityContextCase.OPEN_HOME -> "open_home" ActivityContextCase.OPEN_PROFILE_CHOOSER -> "open_profile_chooser" + ActivityContextCase.REACH_INVESTED_ENGAGEMENT -> "reached_invested_engagement" ActivityContextCase.INSTALL_ID_FOR_FAILED_ANALYTICS_LOG -> "failed_analytics_log" ActivityContextCase.ACTIVITYCONTEXT_NOT_SET -> "unknown_activity_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 3b3251bc852..8d0ccb69a04 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 @@ -51,6 +51,7 @@ class StandardEventTypeToHumanReadableNameConverterImpl @Inject constructor() : ActivityContextCase.DELETE_PROFILE_CONTEXT -> "delete_profile" ActivityContextCase.OPEN_HOME -> "open_home_screen" ActivityContextCase.OPEN_PROFILE_CHOOSER -> "open_profile_chooser_screen" + ActivityContextCase.REACH_INVESTED_ENGAGEMENT -> "reach_invested_engagement" ActivityContextCase.INSTALL_ID_FOR_FAILED_ANALYTICS_LOG, ActivityContextCase.ACTIVITYCONTEXT_NOT_SET -> "ERROR_internal_logging_failure" } diff --git a/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt b/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt index 91557d66f93..932a727e869 100644 --- a/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt +++ b/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt @@ -41,6 +41,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REV import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REVISION_TAB import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STORY_ACTIVITY import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_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.SOLUTION_OFFERED_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT @@ -1464,6 +1465,56 @@ class EventBundleCreatorTest { assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE) } + @Test + fun testFillEventBundle_reachInvestedEngagementEvent_studyOff_fillsAllFieldsAndReturnsName() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val eventLog = createEventLog(context = createReachInvestedEngagementContext()) + + val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle) + assertThat(typeName).isEqualTo("reach_invested_engagement") + assertThat(bundle).hasSize(12) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("essential") + assertThat(bundle).integer("event_type").isEqualTo(REACH_INVESTED_ENGAGEMENT.number) + assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION) + assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME) + assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE) + assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID) + assertThat(bundle).string("story_id").isEqualTo(TEST_STORY_ID) + assertThat(bundle).string("exploration_id").isEqualTo(TEST_EXPLORATION_ID) + assertThat(bundle).string("session_id").isEqualTo(TEST_LEARNER_SESSION_ID) + assertThat(bundle).string("exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR) + assertThat(bundle).string("state_name").isEqualTo(TEST_STATE_NAME) + } + + @Test + fun testFillEventBundle_reachInvestedEngagementEvent_studyOn_fillsNonSensitiveDataAndRetsName() { + setUpTestApplicationComponentWithLearnerAnalyticsStudy() + val bundle = Bundle() + + val eventLog = createEventLog(context = createReachInvestedEngagementContext()) + + val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle) + assertThat(typeName).isEqualTo("reach_invested_engagement") + assertThat(bundle).hasSize(14) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("essential") + assertThat(bundle).integer("event_type").isEqualTo(REACH_INVESTED_ENGAGEMENT.number) + assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION) + assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME) + assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE) + assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID) + assertThat(bundle).string("story_id").isEqualTo(TEST_STORY_ID) + assertThat(bundle).string("exploration_id").isEqualTo(TEST_EXPLORATION_ID) + assertThat(bundle).string("session_id").isEqualTo(TEST_LEARNER_SESSION_ID) + assertThat(bundle).string("exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR) + assertThat(bundle).string("state_name").isEqualTo(TEST_STATE_NAME) + assertThat(bundle).string("ld_learner_id").isEqualTo(TEST_LEARNER_ID) + assertThat(bundle).string("ld_install_id").isEqualTo(TEST_INSTALLATION_ID) + } + @Test fun testFillEventBundle_failedEventInstallId_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() { setUpTestApplicationComponentWithoutLearnerAnalyticsStudy() @@ -1774,6 +1825,10 @@ class EventBundleCreatorTest { private fun createOpenProfileChooserContext() = createEventContext(value = true, EventContextBuilder::setOpenProfileChooser) + private fun createReachInvestedEngagementContext( + explorationContext: ExplorationContext = createExplorationContext() + ) = createEventContext(explorationContext, EventContextBuilder::setReachInvestedEngagement) + private fun createInstallationIdForFailedAnalyticsLogContext( installationId: String = TEST_INSTALLATION_ID ) = createEventContext(installationId, EventContextBuilder::setInstallIdForFailedAnalyticsLog)