Skip to content

Commit

Permalink
App Analytics Implementation (#297)
Browse files Browse the repository at this point in the history
* App Analytics Implementation

* PR feedback

* Remove unused imports
  • Loading branch information
cristhianescobar authored Jul 22, 2024
1 parent e9d52fc commit b5dea14
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 6 deletions.
4 changes: 2 additions & 2 deletions android/app-newm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ android {
applicationId = "io.newm"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 4
versionName = "0.2.3"
versionCode = 5
versionName = "0.2.5"
testInstrumentationRunner = "io.newm.NewmAndroidJUnitRunner"
testApplicationId = "io.newm.test"
}
Expand Down
10 changes: 7 additions & 3 deletions android/app-newm/src/main/java/io/newm/NewmApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import coil.ImageLoaderFactory
import io.newm.BuildConfig.*
import io.newm.di.android.androidModules
import io.newm.di.android.viewModule
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.utils.AndroidNewmAppAnalyticsTracker
import io.newm.utils.AndroidNewmAppLogger
import io.newm.utils.NewmImageLoaderFactory
import io.sentry.Hint
Expand All @@ -27,14 +29,15 @@ open class NewmApplication : Application(), ImageLoaderFactory {

private val logout: Logout by inject()
private val logger: NewmAppLogger by inject()
private val analyticsTracker: NewmAppAnalyticsTracker by inject()
private val imageLoaderFactory by lazy { NewmImageLoaderFactory(this) }
private val config: NewmSharedBuildConfig by inject()

override fun onCreate() {
super.onCreate()
initKoin()
logout.register()
setupSentry()
bindClientImplementations()
}

private fun initKoin() {
Expand All @@ -49,7 +52,7 @@ open class NewmApplication : Application(), ImageLoaderFactory {
}
}

private fun setupSentry() {
private fun bindClientImplementations() {
SentryAndroid.init(
this
) { options: SentryAndroidOptions ->
Expand All @@ -64,7 +67,8 @@ open class NewmApplication : Application(), ImageLoaderFactory {
}
}
}
logger.setClientLogger(AndroidNewmAppLogger())
analyticsTracker.setClientAnalyticsTracker(AndroidNewmAppAnalyticsTracker(logger))
logger.setClientLogger(AndroidNewmAppLogger(analyticsTracker))
}

override fun newImageLoader(): ImageLoader {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.newm.utils

import androidx.core.os.bundleOf
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

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

private val firebaseAnalytics = Firebase.analytics

companion object {
private const val MAX_EVENT_NAME_LENGTH =
32 // Firebase allows 40, but using 32 for readability
private const val TAG = "AnalyticsTracker" // Tag for logging
}

override fun trackEvent(eventName: String, properties: Map<String, Any?>?) {
val safeEventName = validateEventName(eventName)
if (safeEventName == null) {
logger.info(TAG, "Invalid event name: $eventName. Event not tracked.")
return
}
val bundle = bundleOf(*properties?.toList()?.toTypedArray().orEmpty())
firebaseAnalytics.logEvent(safeEventName, bundle)
}

override fun trackScreenView(screenName: String) {
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
param(FirebaseAnalytics.Param.SCREEN_CLASS, screenName) // Optional, if relevant
}
}

override fun setUserId(userId: String) {
firebaseAnalytics.setUserId(userId)
}

override fun setUserProperty(name: String, value: String) {
firebaseAnalytics.setUserProperty(name, value)
}

override fun trackScroll(startPosition: Int, endPosition: Int, screenName: String) {
trackEvent(
"scroll", mapOf(
"scroll_position_start" to startPosition,
"scroll_position_end" to endPosition,
"screen_name" to screenName
)
)
}

override fun trackButtonInteraction(buttonName: String, eventType: String) {
trackEvent(eventType, mapOf("button_name" to buttonName))
}

override fun trackPlayButtonClick(songId: String, songName: String) {
trackEvent(
"play_button_click", mapOf(
"song_id" to songId,
"song_name" to songName
)
)
}

override fun trackAppLaunch() {
trackEvent("app_launch", null)
}

override fun trackAppClose() {
trackEvent("app_close", null)
}

override fun trackUserScroll(percentage: Double) {
trackEvent("user_scroll", mapOf("scroll_percentage" to percentage))
}

private fun validateEventName(name: String): String? {
if (name.startsWith("firebase_") || name.startsWith("google_") || name.startsWith("ga_")) {
return null
}

val cleanedName = name.replace(Regex("[^A-Za-z0-9_]"), "_")
return if (cleanedName.length > MAX_EVENT_NAME_LENGTH) {
cleanedName.substring(0, MAX_EVENT_NAME_LENGTH)
} else {
cleanedName
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package io.newm.utils

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

/**
* An Android-specific implementation of [AppLogger] that logs messages
* using Android's [Log] class and integrates with Sentry for error tracking.
*/
internal class AndroidNewmAppLogger : AppLogger {
internal class AndroidNewmAppLogger(private val analyticsTracker: NewmAppAnalyticsTracker) : AppLogger {

/**
* Sets the user identifier for the logging context and Sentry.
Expand All @@ -21,6 +22,7 @@ internal class AndroidNewmAppLogger : AppLogger {
Sentry.setUser(User().apply {
id = userId
})
analyticsTracker.setUserId(userId)
}

/**
Expand Down
79 changes: 79 additions & 0 deletions shared/src/commonMain/kotlin/io.newm.shared/AppAnalyticsTracker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.newm.shared

/**
* Interface for tracking analytics events in the application.
*/
interface AppAnalyticsTracker {

/**
* Tracks a custom event with the given name and properties.
*
* @param eventName The name of the event to track.
* @param properties A map of properties associated with the event.
*/
fun trackEvent(eventName: String, properties: Map<String, Any?>?)

/**
* Tracks a screen view event.
*
* @param screenName The name of the screen being viewed.
*/
fun trackScreenView(screenName: String)

/**
* Sets the user ID for tracking purposes.
*
* @param userId The ID of the user.
*/
fun setUserId(userId: String)

/**
* Sets a user property.
*
* @param name The name of the property.
* @param value The value of the property.
*/
fun setUserProperty(name: String, value: String)

/**
* Tracks a scroll event.
*
* @param startPosition The starting position of the scroll.
* @param endPosition The ending position of the scroll.
* @param screenName The name of the screen where the scroll happened.
*/
fun trackScroll(startPosition: Int, endPosition: Int, screenName: String)

/**
* Tracks a button interaction event.
*
* @param buttonName The name of the button being interacted with.
* @param eventType The type of the event (default is "button_click").
*/
fun trackButtonInteraction(buttonName: String, eventType: String = "button_click")

/**
* Tracks a play button click event with song details.
*
* @param songId The ID of the song being played.
* @param songName The name of the song being played.
*/
fun trackPlayButtonClick(songId: String, songName: String)

/**
* Tracks the app launch event.
*/
fun trackAppLaunch()

/**
* Tracks the app close event.
*/
fun trackAppClose()

/**
* Tracks a user scroll event with a scroll percentage.
*
* @param percentage The percentage of the scroll.
*/
fun trackUserScroll(percentage: Double)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.newm.shared

class NewmAppAnalyticsTracker : AppAnalyticsTracker {

private var appAnalyticsTracker: AppAnalyticsTracker? = null

fun setClientAnalyticsTracker(clientTracker: AppAnalyticsTracker) {
appAnalyticsTracker = clientTracker
}

override fun trackEvent(eventName: String, properties: Map<String, Any?>?) {
appAnalyticsTracker?.trackEvent(eventName, properties)
}

override fun trackScreenView(screenName: String) {
appAnalyticsTracker?.trackScreenView(screenName)
}

override fun setUserId(userId: String) {
appAnalyticsTracker?.setUserId(userId)
}

override fun setUserProperty(name: String, value: String) {
appAnalyticsTracker?.setUserProperty(name, value)
}

override fun trackScroll(startPosition: Int, endPosition: Int, screenName: String) {
appAnalyticsTracker?.trackScroll(startPosition, endPosition, screenName)
}

override fun trackButtonInteraction(buttonName: String, eventType: String) {
appAnalyticsTracker?.trackButtonInteraction(buttonName, eventType)
}

override fun trackPlayButtonClick(songId: String, songName: String) {
appAnalyticsTracker?.trackPlayButtonClick(songId, songName)
}

override fun trackAppLaunch() {
appAnalyticsTracker?.trackAppLaunch()
}

override fun trackAppClose() {
appAnalyticsTracker?.trackAppClose()
}

override fun trackUserScroll(percentage: Double) {
appAnalyticsTracker?.trackUserScroll(percentage)
}
}
2 changes: 2 additions & 0 deletions shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.newm.shared.di

import io.ktor.client.engine.HttpClientEngine
import io.newm.shared.NewmAppAnalyticsTracker
import io.newm.shared.NewmAppLogger
import io.newm.shared.config.NewmSharedBuildConfig
import io.newm.shared.config.NewmSharedBuildConfigImpl
Expand Down Expand Up @@ -89,6 +90,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single<NewmSharedBuildConfig> { NewmSharedBuildConfigImpl() }
single { TokenManager(get()) }
single { NewmAppLogger() }
single { NewmAppAnalyticsTracker() }
// Internal API Services
single { CardanoWalletAPI(get()) }
single { GenresAPI(get()) }
Expand Down

0 comments on commit b5dea14

Please sign in to comment.