Skip to content

Commit

Permalink
feat: Fullstory Analytics SDK Implementation (openedx#347)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
HamzaIsrar12 authored Jul 9, 2024
1 parent 9f0740f commit 6efdd76
Show file tree
Hide file tree
Showing 36 changed files with 297 additions and 46 deletions.
16 changes: 16 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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

Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/org/openedx/app/AnalyticsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -45,6 +51,12 @@ class AnalyticsManager(
}
}

override fun logScreenEvent(screenName: String, params: Map<String, Any?>) {
services.forEach { analytics ->
analytics.logScreenEvent(screenName, params)
}
}

override fun logEvent(event: String, params: Map<String, Any?>) {
services.forEach { analytics ->
analytics.logEvent(event, params)
Expand Down
9 changes: 1 addition & 8 deletions app/src/main/java/org/openedx/app/AppAnalytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ interface AppAnalytics {
fun logoutEvent(force: Boolean)
fun setUserIdForSession(userId: Long)
fun logEvent(event: String, params: Map<String, Any?>)
fun logScreenEvent(screenName: String, params: Map<String, Any?>)
}

enum class AppAnalyticsEvent(val eventName: String, val biValue: String) {
Expand All @@ -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"
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/org/openedx/app/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
16 changes: 6 additions & 10 deletions app/src/main/java/org/openedx/app/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class FirebaseAnalytics(context: Context) : Analytics {
}

override fun logScreenEvent(screenName: String, params: Map<String, Any?>) {
tracker.logEvent(screenName, params.toBundle())
logger.d { "Firebase Analytics log Screen Event: $screenName + $params" }
}

Expand Down
41 changes: 41 additions & 0 deletions app/src/main/java/org/openedx/app/analytics/FullstoryAnalytics.kt
Original file line number Diff line number Diff line change
@@ -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<String, Any?>) {
logger.d { "Page : $screenName $params" }
FS.page(screenName, params).start()
}

override fun logEvent(eventName: String, params: Map<String, Any?>) {
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"
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()) }
Expand Down
13 changes: 13 additions & 0 deletions auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package org.openedx.auth.presentation
interface AuthAnalytics {
fun setUserIdForSession(userId: Long)
fun logEvent(event: String, params: Map<String, Any?>)
fun logScreenEvent(screenName: String, params: Map<String, Any?>)
}

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"
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class SignInViewModel(

init {
collectAppUpgradeEvent()
logSignInScreenEvent()
}

fun login(username: String, password: String) {
Expand Down Expand Up @@ -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)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class SignUpViewModel(

init {
collectAppUpgradeEvent()
logRegisterScreenEvent()
}

fun getRegistrationFields() {
Expand Down Expand Up @@ -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)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()) }
Expand Down Expand Up @@ -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()) }
Expand Down Expand Up @@ -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()) }
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 6efdd76

Please sign in to comment.