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: add analytics user profile - Part 2 (WPB-8978) #2877

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7ba6c47
feat: add method to set/insert tracking identifier into user metadata…
alexandreferris Jul 8, 2024
27c8163
feat: add MessageContent for DataTransfer with TrackingIdentifier
alexandreferris Jul 8, 2024
27b9220
feat: add DataTransferEventHandler to handle new data transfer events…
alexandreferris Jul 8, 2024
c2a1105
chore: remove needless blank line
alexandreferris Jul 8, 2024
e05e102
feat: add get for current tracking identifier and get and set for pre…
alexandreferris Jul 12, 2024
46d744f
feat: add usage and logic handling for current and previous tracking …
alexandreferris Jul 12, 2024
43db0b6
feat: add tests
alexandreferris Jul 12, 2024
cf4cb58
Merge branch 'feat/add_analytics_user_profile_epic' into feat/add_ana…
alexandreferris Jul 12, 2024
89e8824
feat: add better handling of receiving already existing tracking id a…
alexandreferris Jul 12, 2024
91772f5
feat: add new analytics logger to kalium logger
alexandreferris Jul 12, 2024
6bed9be
feat: add usecase to observe analytics tracking identifier
alexandreferris Jul 15, 2024
1748248
feat: add tests for observeAnalyticsTrackingIdentifierStatus
alexandreferris Jul 15, 2024
ec1d55d
feat: add none AnalyticsIdentifierResult to be used in AR and remove …
alexandreferris Jul 15, 2024
9887094
Merge branch 'develop' into feat/add_analytics_user_profile_part2
alexandreferris Jul 15, 2024
efb3ed5
feat: add proper mapping to observer result and verification on Eithe…
alexandreferris Jul 15, 2024
bdd660c
feat: add tests for observer with new logic
alexandreferris Jul 15, 2024
217d216
chore: add user scope logger
alexandreferris Jul 15, 2024
cfbc9b2
chore: add `as` instantiation of right value for usecase result
alexandreferris Jul 17, 2024
1e16b82
Merge branch 'develop' into feat/add_analytics_user_profile_part2
alexandreferris Jul 17, 2024
41066e2
feat: move AnalyticsIdentifierResult to new data module and update it…
alexandreferris Jul 17, 2024
495e142
feat: add usecase to delete previous tracking identifier
alexandreferris Jul 18, 2024
1273307
Merge branch 'develop' into feat/add_analytics_user_profile_part2
alexandreferris Jul 18, 2024
5f03b4d
Merge branch 'feat/add_analytics_user_profile_epic' into feat/add_ana…
alexandreferris Jul 18, 2024
57b3670
chore: add missing imports
alexandreferris Jul 18, 2024
669aa7d
test: add test for DeletePreviousTrackingIdentifierUseCase
alexandreferris Jul 18, 2024
1cf6287
chore: remove delete previous tracking identifier use case
alexandreferris Jul 19, 2024
34a3839
feat: add extra sealed interface for better handling of analytics ide…
alexandreferris Jul 19, 2024
00050ba
feat: add AnalyticsIdentifierManager to handle migration complete and…
alexandreferris Jul 19, 2024
a466cfa
chore: add missing extension from new sealed interface
alexandreferris Jul 19, 2024
96c5280
test: add tests for AnalyticsIdentifierManager
alexandreferris Jul 19, 2024
7a96b8b
chore: adjust detekt
alexandreferris Jul 19, 2024
f4f34e5
chore: remove unused import
alexandreferris Jul 19, 2024
0c68003
chore: rename user config current tracking identifier
alexandreferris Jul 22, 2024
35144c8
chore: add docs
alexandreferris Jul 22, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ class KaliumLogger(
)

enum class ApplicationFlow {
SYNC, EVENT_RECEIVER, CONVERSATIONS, CONNECTIONS, MESSAGES, SEARCH, SESSION, REGISTER, CLIENTS, CALLING, ASSETS, LOCAL_STORAGE
SYNC, EVENT_RECEIVER, CONVERSATIONS, CONNECTIONS, MESSAGES, SEARCH, SESSION, REGISTER,
CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ interface UserConfigRepository {
fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean)
fun getShouldFetchE2EITrustAnchor(): Boolean
suspend fun setTrackingIdentifier(identifier: String)
suspend fun getTrackingIdentifier(): String?
suspend fun observeTrackingIdentifier(): Flow<Either<StorageFailure, String>>
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved
suspend fun setPreviousTrackingIdentifier(identifier: String)
suspend fun getPreviousTrackingIdentifier(): String?
Copy link
Member

Choose a reason for hiding this comment

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

What is the previousTrackingIdentifier, how does it compare with getTrackingIdentifier?

Every time we setTrackingIdentifier(N) we setPreviousTrackingIdentifier(N-1)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we set the PreviouTrackingIdentifier when we receive a new TrackingIdentifier from remote (meaning a new device of the user was logged in somewhere else and we now need to use this new tracking identifier).

Both identifiers are kept until a proper analytics migration of the new identifier is done, then the previous identifier is deleted.

suspend fun deletePreviousTrackingIdentifier()
}

@Suppress("TooManyFunctions")
Expand Down Expand Up @@ -502,4 +507,25 @@ internal class UserConfigDataSource internal constructor(
userConfigDAO.setTrackingIdentifier(identifier = identifier)
}
}

override suspend fun getTrackingIdentifier(): String? =
userConfigDAO.getTrackingIdentifier()

override suspend fun observeTrackingIdentifier(): Flow<Either<StorageFailure, String>> =
userConfigDAO.observeTrackingIdentifier().wrapStorageRequest()

override suspend fun setPreviousTrackingIdentifier(identifier: String) {
wrapStorageRequest {
userConfigDAO.setPreviousTrackingIdentifier(identifier = identifier)
}
}

override suspend fun getPreviousTrackingIdentifier(): String? =
userConfigDAO.getPreviousTrackingIdentifier()

override suspend fun deletePreviousTrackingIdentifier() {
wrapStorageRequest {
userConfigDAO.deletePreviousTrackingIdentifier()
}
}
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.di.PlatformUserStorageProperties
import com.wire.kalium.logic.di.RootPathsProvider
import com.wire.kalium.logic.di.UserStorageProvider
import com.wire.kalium.logic.feature.analytics.ObserveAnalyticsTrackingIdentifierStatusUseCase
import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver
import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl
import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase
Expand Down Expand Up @@ -1461,6 +1462,9 @@ class UserSessionScope internal constructor(
val observeLegalHoldStateForUser: ObserveLegalHoldStateForUserUseCase
get() = ObserveLegalHoldStateForUserUseCaseImpl(clientRepository)

val observeAnalyticsTrackingIdentifierStatus: ObserveAnalyticsTrackingIdentifierStatusUseCase
get() = ObserveAnalyticsTrackingIdentifierStatusUseCase(userConfigRepository)

suspend fun observeIfE2EIRequiredDuringLogin(): Flow<Boolean?> = clientRepository.observeIsClientRegistrationBlockedByE2EI()

val observeLegalHoldForSelfUser: ObserveLegalHoldStateForSelfUserUseCase
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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.kalium.logic.feature.analytics

import com.benasher44.uuid.uuid4
import com.wire.kalium.logger.KaliumLogger
import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMapRightWithEither
import com.wire.kalium.logic.functional.mapToRightOr
import com.wire.kalium.logic.kaliumLogger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf

/**
* Use case that allows observing if the analytics tracking identifier
* changes, due to receiving a new identifier from another client
* or when it's user's first interaction with analytics.
*/
interface ObserveAnalyticsTrackingIdentifierStatusUseCase {
/**
* Use case [ObserveAnalyticsTrackingIdentifierStatusUseCase] operation
*
* @return a [AnalyticsIdentifierResult]
*/
suspend operator fun invoke(): Flow<AnalyticsIdentifierResult>
}

@Suppress("FunctionNaming")
internal fun ObserveAnalyticsTrackingIdentifierStatusUseCase(
userConfigRepository: UserConfigRepository
) = object : ObserveAnalyticsTrackingIdentifierStatusUseCase {

private val TAG = "ObserveAnalyticsTrackingIdentifierStatusUseCase"
private val logger by lazy { kaliumLogger.withTextTag(KaliumLogger.Companion.ApplicationFlow.ANALYTICS.name) }

override suspend fun invoke(): Flow<AnalyticsIdentifierResult> =
userConfigRepository
.observeTrackingIdentifier()
.distinctUntilChanged()
.flatMapRightWithEither { currentIdentifier: String ->
val result =
userConfigRepository.getPreviousTrackingIdentifier()?.let {
AnalyticsIdentifierResult.MigrationIdentifier(
identifier = currentIdentifier
).also {
logger.i("$TAG Updating Tracking Identifier with migration value.")
}
} ?: AnalyticsIdentifierResult.ExistingIdentifier(
identifier = currentIdentifier
).also {
logger.i("$TAG Updating Tracking Identifier with existing value.")
}

flowOf(Either.Right(result))
}.mapToRightOr(
AnalyticsIdentifierResult.NonExistingIdentifier(
identifier = uuid4().toString()
).also {
logger.i("$TAG Generating new Tracking Identifier value.")
}
)
}

sealed class AnalyticsIdentifierResult {
abstract val identifier: String

// TODO(Analytics): Send DataTransfer message if this
data class NonExistingIdentifier(
override val identifier: String
) : AnalyticsIdentifierResult()

// TODO(Analytics): Just set identifier without migration
data class ExistingIdentifier(
override val identifier: String
) : AnalyticsIdentifierResult()

// TODO(Analytics): Set identifier with migration
data class MigrationIdentifier(
override val identifier: String
) : AnalyticsIdentifierResult()
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package com.wire.kalium.logic.sync.receiver.handler

import com.wire.kalium.logger.KaliumLogger
import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.message.Message
import com.wire.kalium.logic.data.message.MessageContent
Expand All @@ -39,14 +40,34 @@ internal class DataTransferEventHandlerImpl(
message: Message.Signaling,
messageContent: MessageContent.DataTransfer
) {
kaliumLogger.d("MessageContent.DataTransfer | with Identifier : ${messageContent.trackingIdentifier?.identifier} |end|")
// DataTransfer from another user or null tracking identifier shouldn't happen,
// If it happens, it's unnecessary
// and we can squish some performance by skipping it completely
// If it happens, it's unnecessary, and we can squish some performance by skipping it completely
if (message.senderUserId != selfUserId || messageContent.trackingIdentifier == null) return

userConfigRepository.setTrackingIdentifier(
identifier = messageContent.trackingIdentifier!!.identifier
)
val currentTrackingIdentifier = userConfigRepository.getTrackingIdentifier()
val isCurrentDifferentThanReceived = currentTrackingIdentifier != messageContent
.trackingIdentifier!!
.identifier

if (isCurrentDifferentThanReceived) {
currentTrackingIdentifier?.let {
userConfigRepository.setPreviousTrackingIdentifier(identifier = currentTrackingIdentifier)
logger.d("$TAG Moved Current Tracking Identifier to Previous")
}

userConfigRepository.setTrackingIdentifier(
identifier = requireNotNull(
messageContent
.trackingIdentifier
?.identifier
)
)
logger.d("$TAG Tracking Identifier Updated")
}
}

private companion object {
const val TAG = "DataTransferEventHandler"
private val logger by lazy { kaliumLogger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.ANALYTICS) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.kalium.logic.feature.analytics

import app.cash.turbine.test
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangement
import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangementImpl
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertIs

class ObserveAnalyticsTrackingIdentifierStatusUseCaseTest {

@Test
fun givenTrackingIdentifierExists_whenObservingTrackingIdentifier_thenReturnExistingIdentifier() = runTest {
// given
val (_, useCase) = Arrangement().arrange {
withObserveTrackingIdentifier(Either.Right(CURRENT_IDENTIFIER))
withGetPreviousTrackingIdentifier(null)
}

// when
useCase().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.ExistingIdentifier>(item)

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun givenPreviousTrackingIdentifierExists_whenObservingTrackingIdentifier_thenReturnMigrationIdentifier() = runTest {
// given
val (_, useCase) = Arrangement().arrange {
withObserveTrackingIdentifier(Either.Right(CURRENT_IDENTIFIER))
withGetPreviousTrackingIdentifier(PREVIOUS_IDENTIFIER)
}

// when
useCase().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.MigrationIdentifier>(item)

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun givenThereIsNoIdentifier_whenObservingTrackingIdentifier_thenReturnNonExistingIdentifier() = runTest {
// given
val (_, useCase) = Arrangement().arrange {
withObserveTrackingIdentifier(Either.Left(StorageFailure.DataNotFound))
}

// when
useCase().test {
// then
val item = awaitItem()
assertIs<AnalyticsIdentifierResult.NonExistingIdentifier>(item)

cancelAndIgnoreRemainingEvents()
}
}

private companion object {
const val PREVIOUS_IDENTIFIER = "abcd-1234"
const val CURRENT_IDENTIFIER = "efgh-5678"
}

private class Arrangement : UserConfigRepositoryArrangement by UserConfigRepositoryArrangementImpl() {

private val useCase: ObserveAnalyticsTrackingIdentifierStatusUseCase = ObserveAnalyticsTrackingIdentifierStatusUseCase(
userConfigRepository = userConfigRepository
)

fun arrange(block: suspend Arrangement.() -> Unit): Pair<Arrangement, ObserveAnalyticsTrackingIdentifierStatusUseCase> {
runBlocking { block() }
return this to useCase
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class DataTransferEventHandlerTest {
fun givenSelfUserDataTransferContent_whenHandlingEvent_thenSetTrackingIdentifier() = runTest {
// given
val (arrangement, handler) = Arrangement().arrange {
withGetTrackingIdentifier(null)
withSetTrackingIdentifier()
}

Expand Down Expand Up @@ -91,6 +92,58 @@ class DataTransferEventHandlerTest {
}.wasNotInvoked()
}

@Test
fun givenSelfUserHasTrackingIdentifier_whenReceivingNewTrackingIdentifier_thenMoveCurrentToPreviousAndUpdate() = runTest {
// given
val currentIdentifier = "abcd-1234"
val newIdentifier = "efgh-5678"
val (arrangement, handler) = Arrangement().arrange {
withGetTrackingIdentifier(currentIdentifier)
}

// when
handler.handle(
message = MESSAGE,
messageContent = MESSAGE_CONTENT.copy(
trackingIdentifier = MESSAGE_CONTENT.trackingIdentifier?.copy(
identifier = newIdentifier
)
)
)

// then
coVerify {
arrangement.userConfigRepository.setPreviousTrackingIdentifier(currentIdentifier)
}.wasInvoked(exactly = once)

coVerify {
arrangement.userConfigRepository.setTrackingIdentifier(newIdentifier)
}.wasInvoked(exactly = once)
}

@Test
fun givenCurrentIdentifierIsTheSame_whenReceivingNewTrackingIdentifier_thenDoNotUpdateTrackingIdentifier() = runTest {
// given
val (arrangement, handler) = Arrangement().arrange {
withGetTrackingIdentifier(MESSAGE_CONTENT.trackingIdentifier?.identifier)
}

// when
handler.handle(
message = MESSAGE,
messageContent = MESSAGE_CONTENT
)

// then
coVerify {
arrangement.userConfigRepository.setPreviousTrackingIdentifier(any())
}.wasNotInvoked()

coVerify {
arrangement.userConfigRepository.setTrackingIdentifier(any())
}.wasNotInvoked()
}

private companion object {
val CONVERSATION_ID = ConversationId("conversationId", "domain")
val SELF_USER_ID = UserId("selfUserId", "domain")
Expand Down
Loading
Loading