Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): add screen view event #1265

Merged
merged 6 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process",
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" }
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidx-compose-material3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation-compose" }
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidx-navigation-fragment" }
androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation-fragment" }
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation-fragment" }
androidx-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" }
Expand Down
1 change: 1 addition & 0 deletions android/measure/api/measure.api
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public final class sh/measure/android/Measure {
public static final fun trackNavigation (Ljava/lang/String;)V
public static final fun trackNavigation (Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun trackNavigation$default (Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public static final fun trackScreenView (Ljava/lang/String;)V
}

public final class sh/measure/android/config/MeasureConfig : sh/measure/android/config/IMeasureConfig {
Expand Down
1 change: 1 addition & 0 deletions android/measure/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ dependencies {
compileOnly(libs.androidx.compose.runtime.android)
compileOnly(libs.androidx.compose.ui)
compileOnly(libs.androidx.navigation.compose)
compileOnly(libs.androidx.navigation.fragment)
compileOnly(libs.squareup.okhttp)

implementation(libs.kotlinx.serialization.json)
Expand Down
24 changes: 24 additions & 0 deletions android/measure/src/main/java/sh/measure/android/Measure.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,36 @@ object Measure {
*/
@JvmStatic
@JvmOverloads
@Deprecated(
message = "This method will be removed in the next version, use trackScreenView instead",
replaceWith = ReplaceWith("Measure.trackScreenView(screenName)"),
)
fun trackNavigation(to: String, from: String? = null) {
if (isInitialized.get()) {
measure.trackNavigation(to, from)
}
}

/**
* Call when a screen is viewed by the user.
*
* Measure SDK automatically collects screen view events from the Jetpack Navigation library
* for AndroidX Fragment and Compose navigation. But if your app uses a custom navigation
* system, you can use this method to track screen view events to have more context when
* debugging issues.
*
* Example usage:
* ```kotlin
* Measure.trackScreenView("Home")
* ```
*/
@JvmStatic
fun trackScreenView(screenName: String) {
if (isInitialized.get()) {
measure.trackScreenView(screenName)
}
}

/**
* Track a handled exception.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,15 @@ internal class MeasureInternal(measureInitializer: MeasureInitializer) :
userAttributeProcessor.clearUserId()
}

@Deprecated("Use trackScreenView instead")
fun trackNavigation(to: String, from: String?) {
userTriggeredEventCollector.trackNavigation(to, from)
}

fun trackScreenView(screenName: String) {
userTriggeredEventCollector.trackScreenView(screenName)
}

fun trackHandledException(throwable: Throwable) {
userTriggeredEventCollector.trackHandledException(throwable)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ internal object EventType {
const val LOW_MEMORY: String = "low_memory"
const val TRIM_MEMORY: String = "trim_memory"
const val CPU_USAGE: String = "cpu_usage"

@Deprecated("This event type is deprecated and will be removed in the next version. Use SCREEN_VIEW instead.")
const val NAVIGATION: String = "navigation"
const val SCREEN_VIEW: String = "screen_view"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ package sh.measure.android.events

import sh.measure.android.exceptions.ExceptionFactory
import sh.measure.android.navigation.NavigationData
import sh.measure.android.navigation.ScreenViewData
import sh.measure.android.utils.ProcessInfoProvider
import sh.measure.android.utils.TimeProvider

internal interface UserTriggeredEventCollector {
@Deprecated("Use trackScreenView instead")
fun trackNavigation(to: String, from: String?)
fun trackHandledException(throwable: Throwable)
fun trackScreenView(screenName: String)
}

internal class UserTriggeredEventCollectorImpl(
private val eventProcessor: EventProcessor,
private val timeProvider: TimeProvider,
private val processInfoProvider: ProcessInfoProvider,
) : UserTriggeredEventCollector {
@Deprecated("Use trackScreenView instead")
override fun trackNavigation(to: String, from: String?) {
eventProcessor.trackUserTriggered(
data = NavigationData(
Expand All @@ -41,4 +45,12 @@ internal class UserTriggeredEventCollectorImpl(
type = EventType.EXCEPTION,
)
}

override fun trackScreenView(screenName: String) {
eventProcessor.trackUserTriggered(
data = ScreenViewData(name = screenName),
timestamp = timeProvider.currentTimeSinceEpochInMillis,
type = EventType.SCREEN_VIEW,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package sh.measure.android.lifecycle

import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import sh.measure.android.events.EventProcessor
import sh.measure.android.events.EventType
import sh.measure.android.navigation.ScreenViewData
import sh.measure.android.utils.TimeProvider
import sh.measure.android.utils.isClassAvailable

internal class AndroidXFragmentNavigationCollector(
private val eventProcessor: EventProcessor,
private val timeProvider: TimeProvider,
) : FragmentLifecycleAdapter(), NavController.OnDestinationChangedListener {

override fun onFragmentViewCreated(
fm: FragmentManager,
f: Fragment,
v: View,
savedInstanceState: Bundle?,
) {
safelyTrackAndroidxNavChanges(f)
}

override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {
safelyRemoveAndroidxNavChanges(f)
}

override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?,
) {
val displayName = getDisplayName(controller.context, destination.id)
eventProcessor.track(
type = EventType.SCREEN_VIEW,
timestamp = timeProvider.currentTimeSinceEpochInMillis,
data = ScreenViewData(name = displayName),
)
}

private fun safelyTrackAndroidxNavChanges(f: Fragment) {
try {
if (hasAndroidxFragmentNavigation() && f is NavHostFragment) {
f.findNavController().addOnDestinationChangedListener(this)
}
} catch (e: IllegalStateException) {
// ignore
}
}

private fun safelyRemoveAndroidxNavChanges(f: Fragment) {
try {
if (hasAndroidxFragmentNavigation() && f is NavHostFragment) {
f.findNavController().removeOnDestinationChangedListener(this)
}
} catch (e: IllegalStateException) {
// ignore
}
}

private fun getDisplayName(context: Context, id: Int): String {
return if (id <= 0x00FFFFFF) {
id.toString()
} else {
try {
context.resources.getResourceName(id)
} catch (e: Resources.NotFoundException) {
id.toString()
}
}
}

private fun hasAndroidxFragmentNavigation() =
isClassAvailable("androidx.navigation.fragment.NavHostFragment")
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal class LifecycleCollector(
private val fragmentLifecycleCollector by lazy {
FragmentLifecycleCollector(eventProcessor, timeProvider)
}
private val androidXFragmentNavigationCollector by lazy {
AndroidXFragmentNavigationCollector(eventProcessor, timeProvider)
}
private val startedActivities = mutableSetOf<String>()
private var applicationLifecycleStateListener: ApplicationLifecycleStateListener? = null

Expand All @@ -39,6 +42,7 @@ internal class LifecycleCollector(

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
registerFragmentLifecycleCollector(activity)
registerAndroidXFragmentNavigationCollector(activity)
eventProcessor.track(
timestamp = timeProvider.currentTimeSinceEpochInMillis,
type = EventType.LIFECYCLE_ACTIVITY,
Expand Down Expand Up @@ -125,6 +129,15 @@ internal class LifecycleCollector(
}
}

private fun registerAndroidXFragmentNavigationCollector(activity: Activity) {
if (isAndroidXFragmentNavigationAvailable() && activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
androidXFragmentNavigationCollector,
true,
)
}
}

private fun registerFragmentLifecycleCollector(activity: Activity) {
if (isAndroidXFragmentAvailable() && activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
Expand All @@ -136,4 +149,7 @@ internal class LifecycleCollector(

private fun isAndroidXFragmentAvailable() =
isClassAvailable("androidx.fragment.app.FragmentActivity")

private fun isAndroidXFragmentNavigationAvailable() =
isClassAvailable("androidx.navigation.fragment.NavHostFragment")
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ fun NavHostController.withMeasureNavigationListener(): NavHostController {
private class MeasureNavigationObserver(
private val navController: NavController,
) : LifecycleEventObserver {
var lastDestinationRoute: String? = null

private companion object {
private const val SOURCE_ANDROIDX_NAVIGATION = "androidx-navigation"
}

override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_RESUME) {
navController.addOnDestinationChangedListener(destinationChangedListener)
Expand All @@ -55,15 +49,10 @@ private class MeasureNavigationObserver(
val timeProvider = Measure.getTimeProvider() ?: return@let
val eventProcessor = Measure.getEventProcessor() ?: return@let
eventProcessor.track(
type = EventType.NAVIGATION,
type = EventType.SCREEN_VIEW,
timestamp = timeProvider.currentTimeSinceEpochInMillis,
data = NavigationData(
source = SOURCE_ANDROIDX_NAVIGATION,
from = lastDestinationRoute,
to = to,
),
data = ScreenViewData(name = to),
)
lastDestinationRoute = to
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sh.measure.android.navigation
import kotlinx.serialization.Serializable

@Serializable
@Deprecated("This class is deprecated and will be removed in the next version. Use ScreenViewData instead.")
internal data class NavigationData(
/**
* Adds context on how the event was collected.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sh.measure.android.navigation

import kotlinx.serialization.Serializable

/**
* Trigger when a screen is viewed by the user.
*/
@Serializable
internal data class ScreenViewData(
val name: String,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package sh.measure.android.storage

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import sh.measure.android.appexit.AppExit
Expand All @@ -17,6 +16,7 @@ import sh.measure.android.lifecycle.ActivityLifecycleData
import sh.measure.android.lifecycle.ApplicationLifecycleData
import sh.measure.android.lifecycle.FragmentLifecycleData
import sh.measure.android.navigation.NavigationData
import sh.measure.android.navigation.ScreenViewData
import sh.measure.android.networkchange.NetworkChangeData
import sh.measure.android.okhttp.HttpData
import sh.measure.android.performance.CpuUsageData
Expand Down Expand Up @@ -143,6 +143,10 @@ internal fun <T> Event<T>.serializeDataToString(): String {
Json.encodeToString(NavigationData.serializer(), data as NavigationData)
}

EventType.SCREEN_VIEW -> {
Json.encodeToString(ScreenViewData.serializer(), data as ScreenViewData)
}

else -> {
throw IllegalArgumentException("Unknown event type: $type")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import sh.measure.android.lifecycle.ApplicationLifecycleData
import sh.measure.android.lifecycle.FragmentLifecycleData
import sh.measure.android.lifecycle.FragmentLifecycleType
import sh.measure.android.navigation.NavigationData
import sh.measure.android.navigation.ScreenViewData
import sh.measure.android.networkchange.NetworkChangeData
import sh.measure.android.networkchange.NetworkGeneration
import sh.measure.android.okhttp.HttpData
Expand Down Expand Up @@ -457,4 +458,8 @@ internal object TestData {
pid = pid,
)
}

fun getScreenViewData(): ScreenViewData {
return ScreenViewData(name = "screen-name")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class EventExtensionsKtTest {
val navigationEvent = TestData.getNavigationData().toEvent(type = EventType.NAVIGATION)
assert(navigationEvent.serializeDataToString().isNotEmpty())

val screenViewEvent = TestData.getScreenViewData().toEvent(type = EventType.SCREEN_VIEW)
assert(screenViewEvent.serializeDataToString().isNotEmpty())

val lowMemoryEvent = TestData.getLowMemoryData().toEvent(type = EventType.LOW_MEMORY)
assert(lowMemoryEvent.serializeDataToString().isNotEmpty())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SampleApp : Application() {
)
Measure.setUserId("sample-user-sd")
Measure.clearUserId()
Measure.trackNavigation("sample-to", "sample-from")
Measure.trackScreenView("screen-name")
Measure.trackHandledException(RuntimeException("sample-handled-exception"))
/*
Measure.putAttribute("sample-key-1", 123)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import sh.measure.sample.R

Expand All @@ -18,9 +17,6 @@ class AndroidXFragmentNavigationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_android_xfragment_navigation)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
}
}

Expand Down
Loading
Loading