From eb966354703a3b020b86704668a49b0160037089 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Nov 2022 01:32:48 -0800 Subject: [PATCH] Fix #4756: Add support for logging an invested engagement event (#4757) ## Explanation Fixes #4756 This PR introduces a new event for tracking individual play sessions where a learner has reached a level of 'invested' engagement, where 'invested' here is considered to be strong engagement with a likelihood of continuing at least with that play session. This metric is planned to be used as one of the team's conversion metrics to better help track the user marketing pipeline by helping to determine how we can better reach learners who are more likely to reach this level of engagement with lessons (and, thus, hopefully learn what they need to). Note that the event is based on a single play session, not a profile or even a single exploration (so if a user pauses and resumes an exploration, the count for engagement resets **from that point**). Engagement means completing _and_ moving past at minimum 3 cards (which may just be simple 'Continue' button interactions). For simplicity, this PR keeps the new event name the same between the Kenya & non-Kenya styles of naming events. I've verified that the event is logging as expected using Firebase's DebugView: ![image](https://user-images.githubusercontent.com/12983742/203501899-dbccd386-4e04-4966-82bc-a4646b3b742c.png) ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only This PR is introducing a log and has no impact on the UI or UX of the app. --- .../ExplorationProgressController.kt | 13 + .../analytics/LearnerAnalyticsLogger.kt | 9 + .../ExplorationProgressControllerTest.kt | 227 ++++++++++++++++++ .../analytics/LearnerAnalyticsLoggerTest.kt | 24 ++ model/src/main/proto/oppia_logger.proto | 4 + .../testing/FakeAnalyticsEventLogger.kt | 7 +- .../testing/logging/EventLogSubject.kt | 73 ++++-- .../testing/FakeAnalyticsEventLoggerTest.kt | 4 +- .../util/logging/EventBundleCreator.kt | 2 + ...entTypeToHumanReadableNameConverterImpl.kt | 1 + ...entTypeToHumanReadableNameConverterImpl.kt | 1 + .../util/logging/EventBundleCreatorTest.kt | 55 +++++ 12 files changed, 393 insertions(+), 27 deletions(-) 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)