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

fix: Conversations and Profile Views are reported individually (WPB-14942) #3792

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
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
39 changes: 32 additions & 7 deletions app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.navigation.NavDestination
import com.ramcosta.composedestinations.spec.DestinationSpec
import com.wire.android.appLogger
import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
import com.wire.android.navigation.getBaseRoute
import com.wire.android.navigation.toDestination
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.CreateAccountDetailsScreenDestination
Expand Down Expand Up @@ -123,17 +124,31 @@ class CurrentScreenManager @Inject constructor(
}

override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
val currentView = currentScreenState.value.toString()
AnonymousAnalyticsManagerImpl.stopView(currentView)
val currentScreenName = currentScreenName()
AnonymousAnalyticsManagerImpl.stopView(currentScreenName)

val currentItem = destination.toDestination()
currentScreenState.value = CurrentScreen.fromDestination(
currentItem,
arguments,
isApplicationVisibleFlow.value
)

val newView = currentScreenState.value.toString()
AnonymousAnalyticsManagerImpl.recordView(newView)
val newScreenName = currentScreenName()
AnonymousAnalyticsManagerImpl.recordView(newScreenName)
}

private fun currentScreenName() = currentScreenState.value.let { currentScreen ->
when (currentScreen) {
is CurrentScreen.Home,
is CurrentScreen.Conversation,
is CurrentScreen.OtherUserProfile,
is CurrentScreen.ImportMedia,
is CurrentScreen.DeviceManager -> return@let currentScreen.toScreenName()

is CurrentScreen.AuthRelated -> return@let currentScreen.route?.getBaseRoute() ?: currentScreen.toString()
else -> return@let (currentScreen as? CurrentScreen.SomeOther)?.route?.getBaseRoute() ?: currentScreen.toString()
}
}

override fun onCreate(owner: LifecycleOwner) {
Expand Down Expand Up @@ -164,23 +179,31 @@ class CurrentScreenManager @Inject constructor(
sealed class CurrentScreen {

// Home Screen is being displayed
data object Home : CurrentScreen()
data object Home : CurrentScreen() {
override fun toScreenName() = "HomeScreen"
}

// Some Conversation is opened
data class Conversation(val id: ConversationId) : CurrentScreen() {
override fun toString(): String = "Conversation(${id.toString().obfuscateId()})"
override fun toScreenName() = "ConversationScreen"
}

// Another User Profile Screen is opened
data class OtherUserProfile(val id: ConversationId) : CurrentScreen() {
override fun toString(): String = "OtherUserProfile(${id.toString().obfuscateId()})"
override fun toScreenName() = "OtherUserProfileScreen"
}

// Import media screen is opened
data object ImportMedia : CurrentScreen()
data object ImportMedia : CurrentScreen() {
override fun toScreenName() = "ImportMediaScreen"
}

// SelfDevices screen is opened
data object DeviceManager : CurrentScreen()
data object DeviceManager : CurrentScreen() {
override fun toScreenName() = "DeviceManagerScreen"
}

// Auth related screen is opened
data class AuthRelated(val route: String?) : CurrentScreen()
Expand All @@ -191,6 +214,8 @@ sealed class CurrentScreen {
// App is in background (screen is turned off, or covered by another app), non of the screens is visible
data object InBackground : CurrentScreen()

open fun toScreenName(): String = "UnknownScreen"

companion object {
@SuppressLint("RestrictedApi")
@Suppress("ComplexMethod")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ object AnonymousAnalyticsManagerImpl : AnonymousAnalyticsManager {
coroutineScope.launch {
mutex.withLock {
if (!isAnonymousUsageDataEnabled) return@withLock
anonymousAnalyticsRecorder?.recordView(screen)
anonymousAnalyticsRecorder?.recordView(screen.convertToCamelCase())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it really needed to convert this to camel case for countly ?
i mean is the screen names defined there or we can define them as we need on android ?

Copy link
Member Author

@ohassine ohassine Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's for consistency, so we have clean data on the board. Otherwise some screen names are snake_case and some others are in camelCase.

}
}
}
Expand All @@ -195,7 +195,7 @@ object AnonymousAnalyticsManagerImpl : AnonymousAnalyticsManager {
coroutineScope.launch {
mutex.withLock {
if (!isAnonymousUsageDataEnabled) return@withLock
anonymousAnalyticsRecorder?.stopView(screen)
anonymousAnalyticsRecorder?.stopView(screen.convertToCamelCase())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.feature.analytics

/**
* Converts a snake_case string to camelCase.
*/
fun String.convertToCamelCase(): String {
return this
.split('_')
.joinToString("") { it.replaceFirstChar { char -> char.uppercase() } }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.feature.analytics

import org.junit.Assert.assertEquals
import org.junit.Test

class StringExtTest {

@Test
fun `given single word string when converted then return same string`() {
assertEquals("Username", "username".convertToCamelCase())
}

@Test
fun `given string with multiple underscores when converted then return camel case string`() {
assertEquals("ThisIsATestCase", ("this_is_a_test_case").convertToCamelCase())
}

@Test
fun `given empty string when converted then return empty string`() {
assertEquals("", ("").convertToCamelCase())
}

@Test
fun `given string with leading and trailing underscores when converted then return camel case string`() {
assertEquals("LeadingAndTrailing", ("_leading_and_trailing_").convertToCamelCase())
}

@Test
fun `given string with numbers when converted then return camel case string`() {
assertEquals("User123Name", ("user_123_name").convertToCamelCase())
}
}
Loading