Skip to content

Commit

Permalink
Analytics Tracker
Browse files Browse the repository at this point in the history
  • Loading branch information
cristhianescobar committed Aug 18, 2024
1 parent 88224f5 commit 24d2892
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ class DebugNewmApplication : NewmApplication() {
addPlugin(CrashReporterPlugin.getInstance())
addPlugin(DatabasesFlipperPlugin(this@DebugNewmApplication))
addPlugin(NavigationFlipperPlugin.getInstance())
addPlugin(
SharedPreferencesFlipperPlugin(
this@DebugNewmApplication,
"newm_encrypted_shared_prefs"
)
)
}.start()

super.onCreate()
Expand Down
17 changes: 9 additions & 8 deletions android/app-newm/src/main/java/io/newm/Logout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import io.newm.shared.public.usecases.LoginUseCase
import io.newm.shared.public.usecases.UserSessionUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent

Expand All @@ -21,18 +20,20 @@ class Logout(
private val logger: NewmAppLogger
) {

fun signOutUser() = try {
fun signOutUser() {
scope.launch {
loginUseCase.logout()
try {
loginUseCase.logout()
restartApp.run()
logger.info("Logout", "Logout successful")
} catch (e: Exception) {
logger.error("Logout", "Logout failed", e)
}
}
restartApp.run()
logger.info("Logout", "Logout successful")
} catch (e: Exception) {
logger.error("Logout", "Logout failed", e)
}

fun register() {
GlobalScope.launch {
scope.launch {
userSessionUseCase.isLoggedInFlow().collect { isLoggedIn ->
if (!isLoggedIn) {
logger.debug("Logout", "User is not logged in")
Expand Down
46 changes: 23 additions & 23 deletions android/app-newm/src/main/java/io/newm/NewmApplication.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package io.newm

import android.app.Application
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.google.android.recaptcha.Recaptcha
import com.google.firebase.FirebaseApp
import io.newm.BuildConfig.*
import io.newm.di.android.androidModules
import io.newm.di.android.viewModule
import io.newm.feature.login.screen.authproviders.RecaptchaClientProvider
import io.newm.shared.NewmAppAnalyticsTracker
import io.newm.shared.NewmAppLogger
import io.newm.shared.config.NewmSharedBuildConfig
import io.newm.shared.di.initKoin
import io.newm.shared.public.usecases.ForceAppUpdateUseCase
import io.newm.shared.public.analytics.NewmAppAnalyticsTracker
import io.newm.utils.AndroidNewmAppAnalyticsTracker
import io.newm.utils.AndroidNewmAppLogger
import io.newm.utils.AppForegroundBackgroundTracker
import io.newm.utils.ForceAppUpdateViewModel
import io.newm.utils.NewmImageLoaderFactory
import io.sentry.Hint
Expand All @@ -24,41 +25,37 @@ import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.logger.Level


open class NewmApplication : Application(), ImageLoaderFactory {

private val logout: Logout by inject()
private val logger: NewmAppLogger by inject()
private val buildConfig: NewmSharedBuildConfig by inject()
private val recaptchaClientProvider: RecaptchaClientProvider by inject()
private val analyticsTracker: NewmAppAnalyticsTracker by inject()
private val imageLoaderFactory by lazy { NewmImageLoaderFactory(this) }
private val config: NewmSharedBuildConfig by inject()
private val forceAppUpdateViewModel: ForceAppUpdateViewModel by inject()
private val imageLoaderFactory by lazy { NewmImageLoaderFactory(this@NewmApplication) }
private val logger: NewmAppLogger by inject()
private val logout: Logout by inject()
private val recaptchaClientProvider: RecaptchaClientProvider by inject()

override fun onCreate() {
super.onCreate()
initKoin()
FirebaseApp.initializeApp(this)
logout.register()
bindClientImplementations()
initializeRecaptchaClient()
}

private fun initializeRecaptchaClient() {
CoroutineScope(Dispatchers.Default).launch {
Recaptcha.getClient(this@NewmApplication, buildConfig.recaptchaSiteKey, timeout = 50000L)
forceAppUpdateViewModel.viewModelScope.launch {
Recaptcha.getClient(this@NewmApplication, config.recaptchaSiteKey, timeout = 50000L)
.onSuccess { client ->
recaptchaClientProvider.setRecaptchaClient(client)
forceAppUpdateViewModel.checkForUpdates(currentVersion = BuildConfig.VERSION_NAME)

}.onFailure { exception ->
logger.error(
tag = "RecaptchaClient",
Expand All @@ -82,22 +79,25 @@ open class NewmApplication : Application(), ImageLoaderFactory {
}

private fun bindClientImplementations() {
SentryAndroid.init(
this
) { options: SentryAndroidOptions ->
setupSentry()
setupLogger()
registerActivityLifecycleCallbacks(AppForegroundBackgroundTracker(analyticsTracker, logger))
}

private fun setupSentry() {
SentryAndroid.init(this) { options: SentryAndroidOptions ->
options.dsn = config.androidSentryDSN
options.environment = if (DEBUG) "development" else "production"
options.beforeSend =
SentryOptions.BeforeSendCallback { event: SentryEvent, hint: Hint ->
if (SentryLevel.DEBUG == event.level) {
null
} else {
event
}
if (SentryLevel.DEBUG == event.level) null else event
}
}
analyticsTracker.setClientAnalyticsTracker(AndroidNewmAppAnalyticsTracker(logger))
}

private fun setupLogger() {
logger.setClientLogger(AndroidNewmAppLogger(analyticsTracker))
analyticsTracker.setClientAnalyticsTracker(AndroidNewmAppAnalyticsTracker(logger))
}

override fun newImageLoader(): ImageLoader {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ val viewModule = module {
viewModel { params -> MusicPlayerViewModel(params.get(), params.get(), get(), get()) }
single { RecaptchaClientProvider() }

factory { params -> CreateAccountScreenPresenter(params.get(), get(), get(), get(), get()) }
factory { params -> LoginScreenPresenter(params.get(), get(), get(), get()) }
factory { params -> CreateAccountScreenPresenter(params.get(), get(), get(), get(), get(), get()) }
factory { params -> LoginScreenPresenter(params.get(), get(), get(), get(), get()) }
factory { params ->
ResetPasswordScreenPresenter(
params.get(),
Expand All @@ -42,7 +42,7 @@ val viewModule = module {
get(),
get(),
get()
)
, get())
}
single<GoogleSignInLauncher> {
val sharedBuildConfig = get<NewmSharedBuildConfig>()
Expand All @@ -65,7 +65,8 @@ val viewModule = module {
recaptchaClientProvider = get(),
loginUseCase = get(),
activityResultContract = ActivityResultContracts.StartActivityForResult(),
logger = get()
logger = get(),
analyticsTracker = get()
)
}
factory { params ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.analytics.logEvent
import com.google.firebase.ktx.Firebase
import io.newm.shared.AppAnalyticsTracker
import io.newm.shared.NewmAppLogger
import io.newm.shared.public.analytics.AppAnalyticsTracker

/**
* Implementation of [AppAnalyticsTracker] for Android using Firebase Analytics.
*/
class AndroidNewmAppAnalyticsTracker(val logger: NewmAppLogger) : AppAnalyticsTracker {
internal class AndroidNewmAppAnalyticsTracker(val logger: NewmAppLogger) : AppAnalyticsTracker {

private val firebaseAnalytics = Firebase.analytics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.newm.utils

import android.util.Log
import io.newm.shared.AppLogger
import io.newm.shared.NewmAppAnalyticsTracker
import io.newm.shared.public.analytics.NewmAppAnalyticsTracker
import io.sentry.Sentry
import io.sentry.protocol.User

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.newm.utils

import android.app.Activity
import android.app.Application
import android.os.Bundle
import io.newm.shared.NewmAppLogger
import io.newm.shared.public.analytics.NewmAppAnalyticsTracker

class AppForegroundBackgroundTracker(
private val analyticsTracker: NewmAppAnalyticsTracker,
private val logger: NewmAppLogger
) : Application.ActivityLifecycleCallbacks {

private val TAG: String = AppForegroundBackgroundTracker::class.java.simpleName
private var activityReferences = 0
private var isActivityChangingConfigurations = false

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activityReferences == 0 && !isActivityChangingConfigurations) {
// Track initial app launch when first activity is created
try {
analyticsTracker.trackAppLaunch()
} catch (e: Exception) {
logger.error(TAG, "Error tracking app launch", e)
}
}
activityReferences++
}

override fun onActivityStarted(activity: Activity) {
activityReferences++
}

override fun onActivityResumed(activity: Activity) {}

override fun onActivityPaused(activity: Activity) {}

override fun onActivityStopped(activity: Activity) {
activityReferences--
isActivityChangingConfigurations = activity.isChangingConfigurations
if (activityReferences == 0 && !isActivityChangingConfigurations) {
// Track app closure when all activities are stopped and none are changing configurations
try {
analyticsTracker.trackAppClose()
} catch (e: Exception) {
logger.error(TAG, "Error tracking app close", e)
}
}
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}

override fun onActivityDestroyed(activity: Activity) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.newm.feature.login.screen.password.ConfirmPasswordState
import io.newm.feature.login.screen.password.VerificationCodeState
import io.newm.feature.login.screen.password.PasswordState
import io.newm.shared.NewmAppLogger
import io.newm.shared.public.analytics.NewmAppAnalyticsTracker
import io.newm.shared.public.usecases.LoginUseCase
import io.newm.shared.public.usecases.SignupUseCase
import kotlinx.coroutines.launch
Expand All @@ -26,7 +27,8 @@ class CreateAccountScreenPresenter(
private val signupUseCase: SignupUseCase,
private val loginUseCase: LoginUseCase,
private val recaptchaClientProvider: RecaptchaClientProvider,
private val appLogger: NewmAppLogger
private val appLogger: NewmAppLogger,
private val analyticsTracker: NewmAppAnalyticsTracker
) : Presenter<CreateAccountUiState> {

@Composable
Expand All @@ -48,6 +50,7 @@ class CreateAccountScreenPresenter(

return when (step) {
Step.EmailAndPassword -> {
analyticsTracker.trackScreenView("Create Account")
EmailAndPasswordUiState(
emailState = userEmail,
passwordState = password,
Expand All @@ -57,6 +60,7 @@ class CreateAccountScreenPresenter(
) { event ->
when (event) {
SignupFormUiEvent.Next -> {
analyticsTracker.trackButtonInteraction("Next")
require(emailAndPasswordValid) {
"Email and password - next button should not be enabled if any of the fields are invalid"
}
Expand Down Expand Up @@ -87,6 +91,7 @@ class CreateAccountScreenPresenter(
}

Step.EmailVerification -> {
analyticsTracker.trackScreenView("Email Verification")
EmailVerificationUiState(
verificationCode = verificationCode,
nextButtonEnabled = verificationCode.isValid,
Expand All @@ -97,6 +102,7 @@ class CreateAccountScreenPresenter(
require(emailAndPasswordValid && verificationCode.isValid) {
"Email verification - next button should not be enabled if any of the fields are invalid"
}
analyticsTracker.trackButtonInteraction("Next")

coroutineScope.launch {
step = Step.Loading
Expand Down Expand Up @@ -129,7 +135,10 @@ class CreateAccountScreenPresenter(
}
}

Step.Loading -> CreateAccountUiState.Loading
Step.Loading -> {
analyticsTracker.trackScreenView("Loading")
CreateAccountUiState.Loading
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import io.newm.feature.login.screen.authproviders.RecaptchaClientProvider
import io.newm.feature.login.screen.email.EmailState
import io.newm.feature.login.screen.password.PasswordState
import io.newm.shared.NewmAppLogger
import io.newm.shared.public.analytics.NewmAppAnalyticsTracker
import io.newm.shared.public.usecases.LoginUseCase
import kotlinx.coroutines.launch
import kotlin.math.log

class LoginScreenPresenter(
private val navigator: Navigator,
private val loginUseCase: LoginUseCase,
private val recaptchaClientProvider: RecaptchaClientProvider,
private val logger: NewmAppLogger
private val logger: NewmAppLogger,
private val analyticsTracker: NewmAppAnalyticsTracker
) : Presenter<LoginScreenUiState> {
@Composable
override fun present(): LoginScreenUiState {
Expand All @@ -47,6 +48,7 @@ class LoginScreenPresenter(
eventSink = { event ->
when (event) {
LoginUiEvent.OnLoginClick -> {
analyticsTracker.trackButtonInteraction("Login")
coroutineScope.launch {
errorMessage = null

Expand All @@ -73,7 +75,10 @@ class LoginScreenPresenter(
}
}

LoginUiEvent.ForgotPasswordClick -> navigator.goTo(ResetPasswordScreen(email.text))
LoginUiEvent.ForgotPasswordClick -> {
analyticsTracker.trackButtonInteraction("Forgot your password password?")
navigator.goTo(ResetPasswordScreen(email.text))
}
}
}
)
Expand Down
Loading

0 comments on commit 24d2892

Please sign in to comment.