Skip to content

Commit

Permalink
feat: countly cherrypick (WPB-8978) (#3270)
Browse files Browse the repository at this point in the history
Signed-off-by: alexandreferris <[email protected]>
  • Loading branch information
alexandreferris authored Aug 2, 2024
1 parent 7c917d4 commit 9fa30d7
Show file tree
Hide file tree
Showing 18 changed files with 655 additions and 65 deletions.
35 changes: 21 additions & 14 deletions app/src/main/kotlin/com/wire/android/WireApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import android.os.StrictMode
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import co.touchlab.kermit.platformLogWriter
import com.wire.android.analytics.ObserveCurrentSessionAnalyticsUseCase
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.di.ApplicationScope
Expand All @@ -45,16 +46,12 @@ import com.wire.kalium.logger.KaliumLogLevel
import com.wire.kalium.logger.KaliumLogger
import com.wire.kalium.logic.CoreLogger
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import dagger.Lazy
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
Expand Down Expand Up @@ -195,21 +192,31 @@ class WireApplication : BaseApp() {
enableDebugLogging = BuildConfig.DEBUG
)

val isAnonymousUsageDataEnabledFlow = coreLogic.get().getGlobalScope().session.currentSessionFlow()
.flatMapLatest { sessionResult ->
if (sessionResult is CurrentSessionResult.Success && sessionResult.accountInfo.isValid()) {
userDataStoreProvider.get().getOrCreate(sessionResult.accountInfo.userId).isAnonymousUsageDataEnabled()
} else {
flowOf(false)
}
}
.distinctUntilChanged()
val analyticsResultFlow = ObserveCurrentSessionAnalyticsUseCase(
currentSessionFlow = coreLogic.get().getGlobalScope().session.currentSessionFlow(),
isUserTeamMember = {
coreLogic.get().getSessionScope(it).team.isSelfATeamMember()
},
observeAnalyticsTrackingIdentifierStatusFlow = {
coreLogic.get().getSessionScope(it).observeAnalyticsTrackingIdentifierStatus()
},
analyticsIdentifierManagerProvider = {
coreLogic.get().getSessionScope(it).analyticsIdentifierManager
},
userDataStoreProvider = userDataStoreProvider.get()
).invoke()

AnonymousAnalyticsManagerImpl.init(
context = this,
analyticsSettings = analyticsSettings,
isEnabledFlow = isAnonymousUsageDataEnabledFlow,
analyticsResultFlow = analyticsResultFlow,
anonymousAnalyticsRecorder = anonymousAnalyticsRecorder,
propagationHandler = { manager, identifier ->
manager.propagateTrackingIdentifier(identifier)
},
migrationHandler = { manager ->
manager.onMigrationComplete()
},
dispatcher = Dispatchers.IO
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Wire
* Copyright (C) 2024 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.analytics

import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.feature.analytics.model.AnalyticsResult
import com.wire.kalium.logic.data.analytics.AnalyticsIdentifierResult
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.analytics.AnalyticsIdentifierManager
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf

interface ObserveCurrentSessionAnalyticsUseCase {

/**
* Observes a flow of AnalyticsResult of type AnalyticsIdentifierManager
* returning the current result for analytics:
* - newly generated / existing / migration
*
* to be used in analytics user profile device setting.
*/
operator fun invoke(): Flow<AnalyticsResult<AnalyticsIdentifierManager>>
}

@Suppress("FunctionNaming")
fun ObserveCurrentSessionAnalyticsUseCase(
currentSessionFlow: Flow<CurrentSessionResult>,
isUserTeamMember: suspend (UserId) -> Boolean,
observeAnalyticsTrackingIdentifierStatusFlow: suspend (UserId) -> Flow<AnalyticsIdentifierResult>,
analyticsIdentifierManagerProvider: (UserId) -> AnalyticsIdentifierManager,
userDataStoreProvider: UserDataStoreProvider
) = object : ObserveCurrentSessionAnalyticsUseCase {

private var previousAnalyticsResult: AnalyticsIdentifierResult? = null

override fun invoke(): Flow<AnalyticsResult<AnalyticsIdentifierManager>> =
currentSessionFlow
.flatMapLatest {
if (it is CurrentSessionResult.Success && it.accountInfo.isValid()) {
val userId = it.accountInfo.userId
val isTeamMember = isUserTeamMember(userId)
val analyticsIdentifierManager = analyticsIdentifierManagerProvider(userId)

combine(
observeAnalyticsTrackingIdentifierStatusFlow(userId)
.filter { currentIdentifierResult ->
val currentResult = (currentIdentifierResult as? AnalyticsIdentifierResult.Enabled)
val previousResult = (previousAnalyticsResult as? AnalyticsIdentifierResult.Enabled)

currentIdentifierResult != previousAnalyticsResult &&
currentResult?.identifier != previousResult?.identifier
},
userDataStoreProvider.getOrCreate(userId).isAnonymousUsageDataEnabled()
) { identifierResult, enabled ->
previousAnalyticsResult = identifierResult

if (enabled) {
AnalyticsResult(
identifierResult = identifierResult,
isTeamMember = isTeamMember,
manager = analyticsIdentifierManager
)
} else {
AnalyticsResult(
identifierResult = AnalyticsIdentifierResult.Disabled,
isTeamMember = isTeamMember,
manager = analyticsIdentifierManager
)
}
}
} else {
flowOf(
AnalyticsResult<AnalyticsIdentifierManager>(
identifierResult = AnalyticsIdentifierResult.Disabled,
isTeamMember = false,
manager = null
)
)
}
}
.distinctUntilChanged()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Wire
* Copyright (C) 2024 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.analytics

import app.cash.turbine.test
import com.wire.android.assertIs
import com.wire.android.datastore.UserDataStore
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.framework.TestUser
import com.wire.kalium.logic.data.analytics.AnalyticsIdentifierResult
import com.wire.kalium.logic.data.auth.AccountInfo
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.analytics.AnalyticsIdentifierManager
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.junit.Test

class ObserveCurrentSessionAnalyticsUseCaseTest {

@Test
fun givenThereIsNoValidSession_whenObservingCurrentSessionAnalytics_thenDisabledAnalyticsResultIsReturned() = runTest {
// given
val (_, useCase) = Arrangement().apply {
setCurrentSession(CurrentSessionResult.Failure.SessionNotFound)
}.arrange()

// when
useCase.invoke().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.Disabled>(item.identifierResult)
assertEquals(false, item.isTeamMember)
assertEquals(null, item.manager)
}
}

@Test
fun givenThereIsAValidSession_whenObservingCurrentSessionAnalytics_thenExistingIdentifierAnalyticsResultIsReturned() = runTest {
// given
val (_, useCase) = Arrangement()
.withIsAnonymousUsageDataEnabled(true)
.apply {
setCurrentSession(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id)))
setIsTeamMember(TestUser.SELF_USER.id)
setObservingTrackingIdentifierStatus(AnalyticsIdentifierResult.ExistingIdentifier(Arrangement.CURRENT_TRACKING_IDENTIFIER))
}.arrange()

// when
useCase.invoke().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.ExistingIdentifier>(item.identifierResult)
assertEquals(true, item.isTeamMember)
}
}

@Test
fun givenThereIsAValidSessionAndDisabledUsageData_whenObservingCurrentSessionAnalytics_thenDisabledAnalyticsResultIsReturned() =
runTest {
// given
val (_, useCase) = Arrangement()
.withIsAnonymousUsageDataEnabled(false)
.apply {
setCurrentSession(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id)))
setIsTeamMember(TestUser.SELF_USER.id)
setObservingTrackingIdentifierStatus(
AnalyticsIdentifierResult.ExistingIdentifier(Arrangement.CURRENT_TRACKING_IDENTIFIER)
)
}.arrange()

// when
useCase.invoke().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.Disabled>(item.identifierResult)
assertEquals(true, item.isTeamMember)
assertEquals(true, item.manager != null)
}
}

@Test
fun givenUserSwitchAccount_whenObservingCurrentSessionAnalytics_thenExistingIdentifierAnalyticsResultIsReturned() = runTest {
// given
val (arrangement, useCase) = Arrangement()
.withIsAnonymousUsageDataEnabled(true)
.apply {
setCurrentSession(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id)))
setIsTeamMember(TestUser.SELF_USER.id)
setObservingTrackingIdentifierStatus(AnalyticsIdentifierResult.ExistingIdentifier(Arrangement.CURRENT_TRACKING_IDENTIFIER))
}.arrange()

// when
useCase.invoke().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.ExistingIdentifier>(item.identifierResult)
assertEquals(true, item.isTeamMember)

// when changing user
arrangement.setCurrentSession(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.OTHER_USER.id)))
arrangement.setObservingTrackingIdentifierStatus(
AnalyticsIdentifierResult.ExistingIdentifier(Arrangement.OTHER_TRACKING_IDENTIFIER)
)
arrangement.withIsAnonymousUsageDataEnabled(true)

// then
val nextItem = awaitItem()
assertIs<AnalyticsIdentifierResult.ExistingIdentifier>(nextItem.identifierResult)
assertEquals(false, nextItem.isTeamMember)
}
}

private class Arrangement {

@MockK
private lateinit var userDataStore: UserDataStore

@MockK
private lateinit var userDataStoreProvider: UserDataStoreProvider

@MockK
private lateinit var analyticsIdentifierManager: AnalyticsIdentifierManager

private val currentSessionChannel = Channel<CurrentSessionResult>(Channel.UNLIMITED)

private val analyticsTrackingIdentifierStatusChannel = Channel<AnalyticsIdentifierResult>(Channel.UNLIMITED)

private val teamMembers = mutableSetOf<UserId>()

private val isTeamMember: (UserId) -> Boolean = { teamMembers.contains(it) }

init {
// Tests setup
MockKAnnotations.init(this, relaxUnitFun = true)
}

suspend fun setCurrentSession(result: CurrentSessionResult) {
currentSessionChannel.send(result)
}

fun setIsTeamMember(userId: UserId) {
teamMembers.add(userId)
}

suspend fun setObservingTrackingIdentifierStatus(result: AnalyticsIdentifierResult) {
analyticsTrackingIdentifierStatusChannel.send(result)
}

fun withIsAnonymousUsageDataEnabled(result: Boolean): Arrangement = apply {
every { userDataStoreProvider.getOrCreate(any()) } returns userDataStore
coEvery { userDataStore.isAnonymousUsageDataEnabled() } returns flowOf(result)
}

var useCase: ObserveCurrentSessionAnalyticsUseCase = ObserveCurrentSessionAnalyticsUseCase(
currentSessionFlow = currentSessionChannel.receiveAsFlow(),
isUserTeamMember = isTeamMember,
observeAnalyticsTrackingIdentifierStatusFlow = {
analyticsTrackingIdentifierStatusChannel.receiveAsFlow()
},
analyticsIdentifierManagerProvider = {
analyticsIdentifierManager
},
userDataStoreProvider = userDataStoreProvider
)

fun arrange() = this to useCase

companion object {
const val CURRENT_TRACKING_IDENTIFIER = "abcd-1234"
const val OTHER_TRACKING_IDENTIFIER = "aaaa-bbbb-1234"
}
}
}
1 change: 1 addition & 0 deletions core/analytics-enabled/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies {
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)

implementation("com.wire.kalium:kalium-data")
api(project(":core:analytics"))

val composeBom = platform(libs.compose.bom)
Expand Down
Loading

0 comments on commit 9fa30d7

Please sign in to comment.