From 6efdd76c62f07acbc404db377c43cba67a8e8caa Mon Sep 17 00:00:00 2001 From: Hamza Israr <71447999+HamzaIsrar12@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:36:19 +0500 Subject: [PATCH] feat: Fullstory Analytics SDK Implementation (#347) * feat: Fullstory Analytics SDK Implementation We have introduced the Fullstory Analytics Provider, which includes three main methods: Identify: This method identifies the user by passing a userID (uid). Additionally, it includes a displayName for use on the Fullstory dashboard. Event: This method records custom app events. Page: This method functions similarly to a screen event, tracking page views. Fixes: LEARNER-10041 * feat: Add screen event method to the Analytics Manager Fixes: LEARNER-10041 * fix: Course Home Tabs Events Fixes: LEARNER-10041 * chore: Discovery Screen Events Fixes: LEARNER-10041 * chore: Main Dashboard Screen Events Fixes: LEARNER-10041 * chore: Auth Screen Events Fixes: LEARNER-10041 * chore: Profile Screen Events Fixes: LEARNER-10041 * chore: Course Screen Events Fixes: LEARNER-10041 * fix: PLS Banner Multiple Events Fixes: LEARNER-10041 * chore: Logistration Screen Event Fixes: LEARNER-10041 * refactor: Optimize code Fixes: LEARNER-10041 --- app/build.gradle | 16 ++++++++ .../java/org/openedx/app/AnalyticsManager.kt | 12 ++++++ .../main/java/org/openedx/app/AppAnalytics.kt | 9 +--- .../main/java/org/openedx/app/MainFragment.kt | 1 - .../java/org/openedx/app/MainViewModel.kt | 16 +++----- .../app/analytics/FirebaseAnalytics.kt | 1 + .../app/analytics/FullstoryAnalytics.kt | 41 +++++++++++++++++++ .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../auth/presentation/AuthAnalytics.kt | 13 ++++++ .../logistration/LogistrationViewModel.kt | 14 +++++++ .../presentation/signin/SignInViewModel.kt | 11 +++++ .../presentation/signup/SignUpViewModel.kt | 11 +++++ .../signin/SignInViewModelTest.kt | 7 ++++ .../signup/SignUpViewModelTest.kt | 5 +++ core/build.gradle | 4 ++ .../java/org/openedx/core/config/Config.kt | 5 +++ .../openedx/core/config/FullstoryConfig.kt | 11 +++++ .../java/org/openedx/core/ui/ComposeCommon.kt | 2 +- .../course/presentation/CourseAnalytics.kt | 1 + .../container/CourseContainerViewModel.kt | 4 +- .../presentation/dates/CourseDatesScreen.kt | 13 +++++- .../handouts/HandoutsViewModel.kt | 4 +- .../container/CourseContainerViewModelTest.kt | 16 ++++---- .../presentation/DashboardAnalytics.kt | 16 ++++++++ .../learn/presentation/LearnFragment.kt | 11 +++++ .../learn/presentation/LearnViewModel.kt | 21 ++++++++++ default_config/dev/config.yaml | 4 ++ default_config/prod/config.yaml | 4 ++ default_config/stage/config.yaml | 4 ++ .../presentation/DiscoveryAnalytics.kt | 1 + .../presentation/WebViewDiscoveryViewModel.kt | 2 +- .../presentation/info/CourseInfoViewModel.kt | 33 ++++++++++----- .../profile/presentation/ProfileAnalytics.kt | 5 +++ .../presentation/edit/EditProfileViewModel.kt | 17 ++++++++ .../edit/EditProfileViewModelTest.kt | 2 + settings.gradle | 4 ++ 36 files changed, 297 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/org/openedx/app/analytics/FullstoryAnalytics.kt create mode 100644 core/src/main/java/org/openedx/core/config/FullstoryConfig.kt diff --git a/app/build.gradle b/app/build.gradle index e0992b266..659730ff0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,11 +3,15 @@ def appId = config.getOrDefault("APPLICATION_ID", "org.openedx.app") def themeDirectory = config.getOrDefault("THEME_DIRECTORY", "openedx") def firebaseConfig = config.get('FIREBASE') def firebaseEnabled = firebaseConfig?.getOrDefault('ENABLED', false) +def fullstoryConfig = config.get("FULLSTORY") +def fullstoryEnabled = fullstoryConfig?.getOrDefault('ENABLED', false) apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' +apply plugin: 'fullstory' + if (firebaseEnabled) { apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' @@ -25,6 +29,18 @@ if (firebaseEnabled) { preBuild.dependsOn(removeGoogleServicesJson) } +if (fullstoryEnabled) { + def fullstoryOrgId = fullstoryConfig?.get("ORG_ID") + + fullstory { + org fullstoryOrgId + composeEnabled true + composeSelectorVersion 4 + enabledVariants 'debug|release' + logcatLevel 'error' + } +} + android { compileSdk 34 diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt index 356a23459..9d8169863 100644 --- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt +++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt @@ -3,6 +3,7 @@ package org.openedx.app import android.content.Context import org.openedx.app.analytics.Analytics import org.openedx.app.analytics.FirebaseAnalytics +import org.openedx.app.analytics.FullstoryAnalytics import org.openedx.app.analytics.SegmentAnalytics import org.openedx.auth.presentation.AuthAnalytics import org.openedx.core.config.Config @@ -29,10 +30,15 @@ class AnalyticsManager( if (config.getFirebaseConfig().enabled) { addAnalyticsTracker(FirebaseAnalytics(context = context)) } + val segmentConfig = config.getSegmentConfig() if (segmentConfig.enabled && segmentConfig.segmentWriteKey.isNotBlank()) { addAnalyticsTracker(SegmentAnalytics(context = context, config = config)) } + + if (config.getFullstoryConfig().isEnabled) { + addAnalyticsTracker(FullstoryAnalytics()) + } } private fun addAnalyticsTracker(analytic: Analytics) { @@ -45,6 +51,12 @@ class AnalyticsManager( } } + override fun logScreenEvent(screenName: String, params: Map) { + services.forEach { analytics -> + analytics.logScreenEvent(screenName, params) + } + } + override fun logEvent(event: String, params: Map) { services.forEach { analytics -> analytics.logEvent(event, params) diff --git a/app/src/main/java/org/openedx/app/AppAnalytics.kt b/app/src/main/java/org/openedx/app/AppAnalytics.kt index 51278ef13..a122e79c1 100644 --- a/app/src/main/java/org/openedx/app/AppAnalytics.kt +++ b/app/src/main/java/org/openedx/app/AppAnalytics.kt @@ -4,6 +4,7 @@ interface AppAnalytics { fun logoutEvent(force: Boolean) fun setUserIdForSession(userId: Long) fun logEvent(event: String, params: Map) + fun logScreenEvent(screenName: String, params: Map) } enum class AppAnalyticsEvent(val eventName: String, val biValue: String) { @@ -15,14 +16,6 @@ enum class AppAnalyticsEvent(val eventName: String, val biValue: String) { "MainDashboard:Discover", "edx.bi.app.main_dashboard.discover" ), - MY_COURSES( - "MainDashboard:My Courses", - "edx.bi.app.main_dashboard.my_course" - ), - MY_PROGRAMS( - "MainDashboard:My Programs", - "edx.bi.app.main_dashboard.my_program" - ), PROFILE( "MainDashboard:Profile", "edx.bi.app.main_dashboard.profile" diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 62857ee9f..4011b3a04 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -46,7 +46,6 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.bottomNavView.setOnItemSelectedListener { when (it.itemId) { R.id.fragmentLearn -> { - viewModel.logMyCoursesTabClickedEvent() binding.viewPager.setCurrentItem(0, false) } diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index f3d62c04f..5cef29361 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -14,7 +14,6 @@ import org.openedx.core.BaseViewModel import org.openedx.core.config.Config import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.NavigationToDiscovery -import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.discovery.presentation.DiscoveryNavigator class MainViewModel( @@ -51,20 +50,17 @@ class MainViewModel( } fun logDiscoveryTabClickedEvent() { - logEvent(AppAnalyticsEvent.DISCOVER) - } - - fun logMyCoursesTabClickedEvent() { - logEvent(AppAnalyticsEvent.MY_COURSES) + logScreenEvent(AppAnalyticsEvent.DISCOVER) } fun logProfileTabClickedEvent() { - logEvent(AppAnalyticsEvent.PROFILE) + logScreenEvent(AppAnalyticsEvent.PROFILE) } - private fun logEvent(event: AppAnalyticsEvent) { - analytics.logEvent(event.eventName, - buildMap { + private fun logScreenEvent(event: AppAnalyticsEvent) { + analytics.logScreenEvent( + screenName = event.eventName, + params = buildMap { put(AppAnalyticsKey.NAME.key, event.biValue) } ) diff --git a/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt b/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt index 503f3d1ef..17d3b3b62 100644 --- a/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt +++ b/app/src/main/java/org/openedx/app/analytics/FirebaseAnalytics.kt @@ -16,6 +16,7 @@ class FirebaseAnalytics(context: Context) : Analytics { } override fun logScreenEvent(screenName: String, params: Map) { + tracker.logEvent(screenName, params.toBundle()) logger.d { "Firebase Analytics log Screen Event: $screenName + $params" } } diff --git a/app/src/main/java/org/openedx/app/analytics/FullstoryAnalytics.kt b/app/src/main/java/org/openedx/app/analytics/FullstoryAnalytics.kt new file mode 100644 index 000000000..11aa26bc7 --- /dev/null +++ b/app/src/main/java/org/openedx/app/analytics/FullstoryAnalytics.kt @@ -0,0 +1,41 @@ +package org.openedx.app.analytics + +import com.fullstory.FS +import com.fullstory.FSSessionData +import org.openedx.core.utils.Logger + +class FullstoryAnalytics : Analytics { + + private val logger = Logger(TAG) + + init { + FS.setReadyListener { sessionData: FSSessionData -> + val sessionUrl = sessionData.currentSessionURL + logger.d { "FullStory Session URL is: $sessionUrl" } + } + } + + override fun logScreenEvent(screenName: String, params: Map) { + logger.d { "Page : $screenName $params" } + FS.page(screenName, params).start() + } + + override fun logEvent(eventName: String, params: Map) { + logger.d { "Event: $eventName $params" } + FS.page(eventName, params).start() + } + + override fun logUserId(userId: Long) { + logger.d { "Identify: $userId" } + FS.identify( + userId.toString(), mapOf( + DISPLAY_NAME to userId + ) + ) + } + + private companion object { + const val TAG = "FullstoryAnalytics" + private const val DISPLAY_NAME = "displayName" + } +} diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 3fb4667df..429d048b9 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -147,7 +147,7 @@ val screenModule = module { ) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { LearnViewModel(get(), get()) } + viewModel { LearnViewModel(get(), get(), get()) } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt b/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt index e87ad9674..40125a18e 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt @@ -3,9 +3,14 @@ package org.openedx.auth.presentation interface AuthAnalytics { fun setUserIdForSession(userId: Long) fun logEvent(event: String, params: Map) + fun logScreenEvent(screenName: String, params: Map) } enum class AuthAnalyticsEvent(val eventName: String, val biValue: String) { + Logistration( + "Logistration", + "edx.bi.app.logistration" + ), DISCOVERY_COURSES_SEARCH( "Logistration:Courses Search", "edx.bi.app.logistration.courses_search" @@ -14,6 +19,14 @@ enum class AuthAnalyticsEvent(val eventName: String, val biValue: String) { "Logistration:Explore All Courses", "edx.bi.app.logistration.explore.all.courses" ), + SIGN_IN( + "Logistration:Sign In", + "edx.bi.app.logistration.signin" + ), + REGISTER( + "Logistration:Register", + "edx.bi.app.logistration.register" + ), REGISTER_CLICKED( "Logistration:Register Clicked", "edx.bi.app.logistration.register.clicked" diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt index e48a5e8be..3306ccfa3 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt @@ -18,6 +18,10 @@ class LogistrationViewModel( private val discoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() + init { + logLogistrationScreenEvent() + } + fun navigateToSignIn(parentFragmentManager: FragmentManager) { router.navigateToSignIn(parentFragmentManager, courseId, null) logEvent(AuthAnalyticsEvent.SIGN_IN_CLICKED) @@ -62,4 +66,14 @@ class LogistrationViewModel( } ) } + + private fun logLogistrationScreenEvent() { + val event = AuthAnalyticsEvent.Logistration + analytics.logScreenEvent( + screenName = event.eventName, + params = buildMap { + put(AuthAnalyticsKey.NAME.key, event.biValue) + } + ) + } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index 9fbd8c2fe..dd03bdaae 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -78,6 +78,7 @@ class SignInViewModel( init { collectAppUpgradeEvent() + logSignInScreenEvent() } fun login(username: String, password: String) { @@ -245,4 +246,14 @@ class SignInViewModel( } ) } + + private fun logSignInScreenEvent() { + val event = AuthAnalyticsEvent.SIGN_IN + analytics.logScreenEvent( + screenName = event.eventName, + params = buildMap { + put(AuthAnalyticsKey.NAME.key, event.biValue) + } + ) + } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt index 42b6bf2d1..0826fca5c 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt @@ -73,6 +73,7 @@ class SignUpViewModel( init { collectAppUpgradeEvent() + logRegisterScreenEvent() } fun getRegistrationFields() { @@ -324,4 +325,14 @@ class SignUpViewModel( } ) } + + private fun logRegisterScreenEvent() { + val event = AuthAnalyticsEvent.REGISTER + analytics.logScreenEvent( + screenName = event.eventName, + params = buildMap { + put(AuthAnalyticsKey.NAME.key, event.biValue) + } + ) + } } diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index d35e34040..a46b371c8 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -87,6 +87,7 @@ class SignInViewModelTest { every { config.getFacebookConfig() } returns FacebookConfig() every { config.getGoogleConfig() } returns GoogleConfig() every { config.getMicrosoftConfig() } returns MicrosoftConfig() + every { analytics.logScreenEvent(any(), any()) } returns Unit } @After @@ -119,6 +120,7 @@ class SignInViewModelTest { coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage val uiState = viewModel.uiState.value @@ -220,6 +222,7 @@ class SignInViewModelTest { coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage val uiState = viewModel.uiState.value @@ -258,6 +261,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 1) { analytics.setUserIdForSession(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } val uiState = viewModel.uiState.value assertFalse(uiState.showProgress) @@ -294,6 +298,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -333,6 +338,7 @@ class SignInViewModelTest { verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { appNotifier.notifier } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage val uiState = viewModel.uiState.value @@ -371,6 +377,7 @@ class SignInViewModelTest { verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { appNotifier.notifier } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage val uiState = viewModel.uiState.value diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt index 5be80557c..f61e1053e 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt @@ -119,6 +119,7 @@ class SignUpViewModelTest { every { config.getGoogleConfig() } returns GoogleConfig() every { config.getMicrosoftConfig() } returns MicrosoftConfig() every { config.getMicrosoftConfig() } returns MicrosoftConfig() + every { analytics.logScreenEvent(any(), any()) } returns Unit } @After @@ -159,6 +160,7 @@ class SignUpViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } @@ -206,6 +208,7 @@ class SignUpViewModelTest { viewModel.register() advanceUntilIdle() verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } coVerify(exactly = 0) { interactor.register(any()) } @@ -245,6 +248,7 @@ class SignUpViewModelTest { advanceUntilIdle() verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -298,6 +302,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.register(any()) } coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } verify(exactly = 1) { appNotifier.notifier } assertFalse(viewModel.uiState.value.validationError) diff --git a/core/build.gradle b/core/build.gradle index 2360efd4d..c18b5ad0c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -114,6 +114,10 @@ dependencies { api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + // Fullstory + api 'com.fullstory:instrumentation-full:1.47.0@aar' + api 'com.fullstory:compose:1.47.0@aar' + // Room api "androidx.room:room-runtime:$room_version" api "androidx.room:room-ktx:$room_version" diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index 57f91ef88..528ff4cc8 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -65,6 +65,10 @@ class Config(context: Context) { return getObjectOrNewInstance(SEGMENT_IO, SegmentConfig::class.java) } + fun getFullstoryConfig(): FullstoryConfig { + return getObjectOrNewInstance(FULLSTORY, FullstoryConfig::class.java) + } + fun getBrazeConfig(): BrazeConfig { return getObjectOrNewInstance(BRAZE, BrazeConfig::class.java) } @@ -158,6 +162,7 @@ class Config(context: Context) { private const val SOCIAL_AUTH_ENABLED = "SOCIAL_AUTH_ENABLED" private const val FIREBASE = "FIREBASE" private const val SEGMENT_IO = "SEGMENT_IO" + private const val FULLSTORY = "FULLSTORY" private const val BRAZE = "BRAZE" private const val FACEBOOK = "FACEBOOK" private const val GOOGLE = "GOOGLE" diff --git a/core/src/main/java/org/openedx/core/config/FullstoryConfig.kt b/core/src/main/java/org/openedx/core/config/FullstoryConfig.kt new file mode 100644 index 000000000..00bc00e81 --- /dev/null +++ b/core/src/main/java/org/openedx/core/config/FullstoryConfig.kt @@ -0,0 +1,11 @@ +package org.openedx.core.config + +import com.google.gson.annotations.SerializedName + +data class FullstoryConfig( + @SerializedName("ENABLED") + val isEnabled: Boolean = false, + + @SerializedName("ORG_ID") + private val orgId: String = "" +) diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 6c57df741..26806897f 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -1253,9 +1253,9 @@ fun RoundTabsBar( .then(border) .clickable { scope.launch { + onTabClicked(index) pagerState.scrollToPage(index) rowState.animateScrollToItem(index) - onTabClicked(index) } } .padding(horizontal = 16.dp), diff --git a/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt b/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt index 8151226c0..0dbe660e5 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt @@ -38,6 +38,7 @@ interface CourseAnalytics { fun finishVerticalBackClickedEvent(courseId: String, courseName: String) fun logEvent(event: String, params: Map) + fun logScreenEvent(screenName: String, params: Map) } enum class CourseAnalyticsEvent(val eventName: String, val biValue: String) { diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index 97045561e..60813d29a 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -420,8 +420,8 @@ class CourseContainerViewModel( } private fun logCourseContainerEvent(event: CourseAnalyticsEvent) { - courseAnalytics.logEvent( - event = event.eventName, + courseAnalytics.logScreenEvent( + screenName = event.eventName, params = buildMap { put(CourseAnalyticsKey.NAME.key, event.biValue) put(CourseAnalyticsKey.COURSE_ID.key, courseId) diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt index 7381402b2..76197b93c 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -212,6 +213,17 @@ private fun CourseDatesUI( HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) + val isPLSBannerAvailable = (uiState as? DatesUIState.Dates) + ?.courseDatesResult + ?.courseBanner + ?.isBannerAvailableForUserType(isSelfPaced) + + LaunchedEffect(key1 = isPLSBannerAvailable) { + if (isPLSBannerAvailable == true) { + onPLSBannerViewed() + } + } + Box( modifier = Modifier .fillMaxSize() @@ -249,7 +261,6 @@ private fun CourseDatesUI( if (courseBanner.isBannerAvailableForUserType(isSelfPaced)) { item { - onPLSBannerViewed() if (windowSize.isTablet) { CourseDatesBannerTablet( modifier = Modifier.padding(top = 16.dp), diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt index 66ba39293..92aaa139d 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt @@ -98,8 +98,8 @@ class HandoutsViewModel( } fun logEvent(event: CourseAnalyticsEvent) { - courseAnalytics.logEvent( - event = event.eventName, + courseAnalytics.logScreenEvent( + screenName = event.eventName, params = buildMap { put(CourseAnalyticsKey.NAME.key, event.biValue) put(CourseAnalyticsKey.COURSE_ID.key, courseId) diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 938d850d2..f049e3751 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -171,12 +171,12 @@ class CourseContainerViewModelTest { ) every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseStructure(any(), any()) } throws UnknownHostException() - every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit + every { analytics.logScreenEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCourseStructure(any(), any()) } - verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } + verify(exactly = 1) { analytics.logScreenEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } val message = viewModel.errorMessage.value assertEquals(noInternet, message) @@ -205,12 +205,12 @@ class CourseContainerViewModelTest { ) every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseStructure(any(), any()) } throws Exception() - every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit + every { analytics.logScreenEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCourseStructure(any(), any()) } - verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } + verify(exactly = 1) { analytics.logScreenEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } val message = viewModel.errorMessage.value assertEquals(somethingWrong, message) @@ -239,12 +239,12 @@ class CourseContainerViewModelTest { ) every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseStructure(any(), any()) } returns courseStructure - every { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit + every { analytics.logScreenEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } returns Unit viewModel.preloadCourseStructure() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCourseStructure(any(), any()) } - verify(exactly = 1) { analytics.logEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } + verify(exactly = 1) { analytics.logScreenEvent(CourseAnalyticsEvent.DASHBOARD.eventName, any()) } assert(viewModel.errorMessage.value == null) assert(!viewModel.refreshing.value) @@ -272,7 +272,7 @@ class CourseContainerViewModelTest { ) every { networkConnection.isOnline() } returns false coEvery { interactor.getCourseStructure(any(), any()) } returns courseStructure - every { analytics.logEvent(any(), any()) } returns Unit + every { analytics.logScreenEvent(any(), any()) } returns Unit coEvery { courseApi.getCourseStructure(any(), any(), any(), any()) } returns courseStructureModel @@ -280,7 +280,7 @@ class CourseContainerViewModelTest { advanceUntilIdle() coVerify(exactly = 0) { courseApi.getCourseStructure(any(), any(), any(), any()) } - verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } assert(viewModel.errorMessage.value == null) assert(!viewModel.refreshing.value) diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt index 6a69e7a65..cf7097a64 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt @@ -1,5 +1,21 @@ package org.openedx.dashboard.presentation interface DashboardAnalytics { + fun logScreenEvent(screenName: String, params: Map) fun dashboardCourseClickedEvent(courseId: String, courseName: String) } + +enum class DashboardAnalyticsEvent(val eventName: String, val biValue: String) { + MY_COURSES( + "MainDashboard:My Courses", + "edx.bi.app.main_dashboard.my_course" + ), + MY_PROGRAMS( + "MainDashboard:My Programs", + "edx.bi.app.main_dashboard.my_program" + ), +} + +enum class DashboardAnalyticsKey(val key: String) { + NAME("name"), +} diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt index 1fc574f41..b1f4bbbb7 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -93,6 +93,17 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { } binding.viewPager.adapter = adapter binding.viewPager.setUserInputEnabled(false) + + binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + if (LearnType.COURSES.ordinal == position) { + viewModel.logMyCoursesTabClickedEvent() + } else { + viewModel.logMyProgramsTabClickedEvent() + } + } + }) } companion object { diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt index 62ee774cb..9a2d17f1e 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -4,11 +4,15 @@ import androidx.fragment.app.FragmentManager import org.openedx.DashboardNavigator import org.openedx.core.BaseViewModel import org.openedx.core.config.Config +import org.openedx.dashboard.presentation.DashboardAnalytics +import org.openedx.dashboard.presentation.DashboardAnalyticsEvent +import org.openedx.dashboard.presentation.DashboardAnalyticsKey import org.openedx.dashboard.presentation.DashboardRouter class LearnViewModel( private val config: Config, private val dashboardRouter: DashboardRouter, + private val analytics: DashboardAnalytics, ) : BaseViewModel() { private val dashboardType get() = config.getDashboardConfig().getType() @@ -21,4 +25,21 @@ class LearnViewModel( val getDashboardFragment get() = DashboardNavigator(dashboardType).getDashboardFragment() val getProgramFragment get() = dashboardRouter.getProgramFragment() + + fun logMyCoursesTabClickedEvent() { + logScreenEvent(DashboardAnalyticsEvent.MY_COURSES) + } + + fun logMyProgramsTabClickedEvent() { + logScreenEvent(DashboardAnalyticsEvent.MY_PROGRAMS) + } + + private fun logScreenEvent(event: DashboardAnalyticsEvent) { + analytics.logScreenEvent( + screenName = event.eventName, + params = buildMap { + put(DashboardAnalyticsKey.NAME.key, event.biValue) + } + ) + } } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index eee22e36d..a97d7c351 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -69,6 +69,10 @@ BRANCH: HOST: '' ALTERNATE_HOST: '' +FULLSTORY: + ENABLED: false + ORG_ID: '' + #Platform names PLATFORM_NAME: "OpenEdX" PLATFORM_FULL_NAME: "OpenEdX" diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index eee22e36d..a97d7c351 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -69,6 +69,10 @@ BRANCH: HOST: '' ALTERNATE_HOST: '' +FULLSTORY: + ENABLED: false + ORG_ID: '' + #Platform names PLATFORM_NAME: "OpenEdX" PLATFORM_FULL_NAME: "OpenEdX" diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index eee22e36d..a97d7c351 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -69,6 +69,10 @@ BRANCH: HOST: '' ALTERNATE_HOST: '' +FULLSTORY: + ENABLED: false + ORG_ID: '' + #Platform names PLATFORM_NAME: "OpenEdX" PLATFORM_FULL_NAME: "OpenEdX" diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt index 4540a0d7f..23994a3fb 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt @@ -5,6 +5,7 @@ interface DiscoveryAnalytics { fun discoveryCourseSearchEvent(label: String, coursesCount: Int) fun discoveryCourseClickedEvent(courseId: String, courseName: String) fun logEvent(event: String, params: Map) + fun logScreenEvent(screenName: String, params: Map) } enum class DiscoveryAnalyticsEvent(val eventName: String, val biValue: String) { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt index f86eef2b8..e786a3970 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt @@ -77,7 +77,7 @@ class WebViewDiscoveryViewModel( event: DiscoveryAnalyticsEvent, courseId: String, ) { - analytics.logEvent( + analytics.logScreenEvent( event.eventName, buildMap { put(DiscoveryAnalyticsKey.NAME.key, event.biValue) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt index 6d41ac4b1..487027e8f 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt @@ -146,11 +146,11 @@ class CourseInfoViewModel( } fun courseInfoClickedEvent(courseId: String) { - logEvent(DiscoveryAnalyticsEvent.COURSE_INFO, courseId) + logScreenEvent(DiscoveryAnalyticsEvent.COURSE_INFO, courseId) } fun programInfoClickedEvent(courseId: String) { - logEvent(DiscoveryAnalyticsEvent.PROGRAM_INFO, courseId) + logScreenEvent(DiscoveryAnalyticsEvent.PROGRAM_INFO, courseId) } fun courseEnrollClickedEvent(courseId: String) { @@ -165,15 +165,26 @@ class CourseInfoViewModel( event: DiscoveryAnalyticsEvent, courseId: String, ) { - analytics.logEvent( - event.eventName, - buildMap { - put(DiscoveryAnalyticsKey.NAME.key, event.biValue) - put(DiscoveryAnalyticsKey.COURSE_ID.key, courseId) - put(DiscoveryAnalyticsKey.CATEGORY.key, CoreAnalyticsKey.DISCOVERY.key) - put(DiscoveryAnalyticsKey.CONVERSION.key, courseId) - } - ) + analytics.logEvent(event.eventName, buildEventDataMap(event, courseId)) + } + + private fun logScreenEvent( + event: DiscoveryAnalyticsEvent, + courseId: String, + ) { + analytics.logScreenEvent(event.eventName, buildEventDataMap(event, courseId)) + } + + private fun buildEventDataMap( + event: DiscoveryAnalyticsEvent, + courseId: String, + ): Map { + return buildMap { + put(DiscoveryAnalyticsKey.NAME.key, event.biValue) + put(DiscoveryAnalyticsKey.COURSE_ID.key, courseId) + put(DiscoveryAnalyticsKey.CATEGORY.key, CoreAnalyticsKey.DISCOVERY.key) + put(DiscoveryAnalyticsKey.CONVERSION.key, courseId) + } } companion object { diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt index 2422ba505..684fc309e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt @@ -2,9 +2,14 @@ package org.openedx.profile.presentation interface ProfileAnalytics { fun logEvent(event: String, params: Map) + fun logScreenEvent(screenName: String, params: Map) } enum class ProfileAnalyticsEvent(val eventName: String, val biValue: String) { + EDIT_PROFILE( + "Profile:Edit Profile", + "edx.bi.app.profile.edit" + ), EDIT_CLICKED( "Profile:Edit Clicked", "edx.bi.app.profile.edit.clicked" diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt index 64cf9789f..211ce2794 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt @@ -67,6 +67,9 @@ class EditProfileViewModel( val showLeaveDialog: LiveData get() = _showLeaveDialog + init { + logProfileScreenEvent(ProfileAnalyticsEvent.EDIT_PROFILE) + } fun updateAccount(fields: Map) { _uiState.value = EditProfileUIState(account, true, isLimitedProfile) @@ -156,4 +159,18 @@ class EditProfileViewModel( } ) } + + private fun logProfileScreenEvent( + event: ProfileAnalyticsEvent, + params: Map = emptyMap(), + ) { + analytics.logScreenEvent( + screenName = event.eventName, + params = buildMap { + put(ProfileAnalyticsKey.NAME.key, event.biValue) + put(ProfileAnalyticsKey.CATEGORY.key, ProfileAnalyticsKey.PROFILE.key) + putAll(params) + } + ) + } } diff --git a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt index bfe6bb0b3..a9b5b0c31 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt @@ -64,6 +64,7 @@ class EditProfileViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { analytics.logScreenEvent(any(), any()) } returns Unit } @After @@ -172,6 +173,7 @@ class EditProfileViewModelTest { advanceUntilIdle() verify(exactly = 1) { analytics.logEvent(any(), any()) } + verify(exactly = 1) { analytics.logScreenEvent(any(), any()) } coVerify(exactly = 1) { interactor.updateAccount(any()) } coVerify(exactly = 1) { interactor.setProfileImage(any(), any()) } diff --git a/settings.gradle b/settings.gradle index 66cb04c11..8f539415d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() + maven { url "https://maven.fullstory.com" } } buildscript { repositories { @@ -10,9 +11,11 @@ pluginManagement { maven { url = uri("https://storage.googleapis.com/r8-releases/raw") } + maven { url "https://maven.fullstory.com" } } dependencies { classpath("com.android.tools:r8:8.2.26") + classpath 'com.fullstory:gradle-plugin-local:1.47.0' } } } @@ -21,6 +24,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url "https://maven.fullstory.com" } maven { url "http://appboy.github.io/appboy-android-sdk/sdk" allowInsecureProtocol = true