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 all 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
@@ -0,0 +1,57 @@
/*
* 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.data.analytics

sealed interface AnalyticsIdentifierResult {
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved

/**
* To be used when there is no user logged in or analytics settings is disabled.
*/
data object Disabled : AnalyticsIdentifierResult

/**
* Wrapper: To be used when there is a user logged in and analytics settings is enabled.
*/
sealed interface Enabled : AnalyticsIdentifierResult {
val identifier: String
}

/**
* To be used when user first login to device, generating a new identifier and sending over to other clients.
*/
data class NonExistingIdentifier(
override val identifier: String
) : Enabled

/**
* To be used when user already has a tracking identifier, meaning no migration will be done.
*/
data class ExistingIdentifier(
override val identifier: String
) : Enabled

/**
* To be used when user is already logged in and receive a new tracking identifier from another client,
* it needs to set received tracking identifier as current identifier with migration.
* (migrate old identifier events to new identifier)
*/
data class MigrationIdentifier(
override val identifier: String
) : Enabled
}
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 @@ -136,7 +136,12 @@ interface UserConfigRepository {
suspend fun clearE2EISettings()
fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean)
fun getShouldFetchE2EITrustAnchor(): Boolean
suspend fun setTrackingIdentifier(identifier: String)
suspend fun setCurrentTrackingIdentifier(newIdentifier: String)
suspend fun getCurrentTrackingIdentifier(): String?
suspend fun observeCurrentTrackingIdentifier(): Flow<Either<StorageFailure, String>>
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 @@ -497,9 +502,30 @@ internal class UserConfigDataSource internal constructor(

override fun getShouldFetchE2EITrustAnchor(): Boolean = userConfigStorage.getShouldFetchE2EITrustAnchorHasRun()

override suspend fun setTrackingIdentifier(identifier: String) {
override suspend fun setCurrentTrackingIdentifier(newIdentifier: String) {
wrapStorageRequest {
userConfigDAO.setTrackingIdentifier(identifier = identifier)
userConfigDAO.setTrackingIdentifier(identifier = newIdentifier)
}
}

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

override suspend fun observeCurrentTrackingIdentifier(): 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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ 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.AnalyticsIdentifierManager
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 @@ -1309,7 +1311,8 @@ class UserSessionScope internal constructor(
private val dataTransferEventHandler: DataTransferEventHandler
get() = DataTransferEventHandlerImpl(
userId,
userConfigRepository
userConfigRepository,
userScopedLogger
)

private val applicationMessageHandler: ApplicationMessageHandler
Expand Down Expand Up @@ -1475,6 +1478,19 @@ class UserSessionScope internal constructor(
val observeLegalHoldStateForUser: ObserveLegalHoldStateForUserUseCase
get() = ObserveLegalHoldStateForUserUseCaseImpl(clientRepository)

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

val analyticsIdentifierManager: AnalyticsIdentifierManager
get() = AnalyticsIdentifierManager(
messages.messageSender,
userConfigRepository,
userId,
clientIdProvider,
selfConversationIdProvider,
userScopedLogger
)

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,104 @@
/*
* 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.cache.SelfConversationIdProvider
import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.data.message.Message
import com.wire.kalium.logic.data.message.MessageContent
import com.wire.kalium.logic.data.message.MessageTarget
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.message.MessageSender
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.foldToEitherWhileRight
import com.wire.kalium.logic.kaliumLogger
import kotlinx.datetime.Clock

interface AnalyticsIdentifierManager {

/**
* When doing a migration of tracking identifier (receive new identifier -> migrate new identifier),
* we should then after migration is complete, delete the previous tracking identifier.
*
* Previous tracking identifier is kept because in case migration or network failure, we still have both values
* to do the correct migration of tracking identifiers.
*/
suspend fun onMigrationComplete()
alexandreferris marked this conversation as resolved.
Show resolved Hide resolved

/**
* When user first login, we generate a new tracking identifier, when this tracking identifier is set,
* we need to send a message to the other clients of the user, to ensure they also use this newly generated identifier.
*/
suspend fun propagateTrackingIdentifier(identifier: String)
}

@Suppress("FunctionNaming", "LongParameterList")
internal fun AnalyticsIdentifierManager(
messageSender: MessageSender,
userConfigRepository: UserConfigRepository,
selfUserId: UserId,
selfClientIdProvider: CurrentClientIdProvider,
selfConversationIdProvider: SelfConversationIdProvider,
defaultLogger: KaliumLogger = kaliumLogger
) = object : AnalyticsIdentifierManager {

private val TAG = "AnalyticsIdentifierManager"
private val logger = defaultLogger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.ANALYTICS)

override suspend fun onMigrationComplete() {
userConfigRepository.deletePreviousTrackingIdentifier()

logger.i("$TAG Previous Tracking Identifier deleted.")
}

override suspend fun propagateTrackingIdentifier(identifier: String) {
val messageContent = MessageContent.DataTransfer(
trackingIdentifier = MessageContent.DataTransfer.TrackingIdentifier(
identifier = identifier
)
)
selfClientIdProvider().flatMap { currentClientId ->
selfConversationIdProvider().flatMap { selfConversationIdList ->
selfConversationIdList.foldToEitherWhileRight(Unit) { selfConversationId, _ ->
val date = Clock.System.now()
val message = Message.Signaling(
id = uuid4().toString(),
content = messageContent,
conversationId = selfConversationId,
date = date,
senderUserId = selfUserId,
senderClientId = currentClientId,
status = Message.Status.Sent,
isSelfMessage = true,
expirationData = null
)

messageSender.sendMessage(
message = message,
messageTarget = MessageTarget.Conversation()
).also {
logger.i("$TAG Tracking Identifier propagated.")
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.data.analytics.AnalyticsIdentifierResult
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMapRightWithEither
import com.wire.kalium.logic.functional.isRight
import com.wire.kalium.logic.kaliumLogger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

/**
* 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,
defaultLogger: KaliumLogger = kaliumLogger,
) = object : ObserveAnalyticsTrackingIdentifierStatusUseCase {

private val TAG = "ObserveAnalyticsTrackingIdentifierStatusUseCase"
private val logger = defaultLogger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.ANALYTICS)

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

flowOf(Either.Right(result))
}.map {
// it's needed, otherwise it will be detected as Flow<Any>
if (it.isRight()) it.value as AnalyticsIdentifierResult
else {
userConfigRepository.getCurrentTrackingIdentifier()?.let { currentIdentifier: String ->
logger.i("$TAG Updating Tracking Identifier with existing value.")
AnalyticsIdentifierResult.ExistingIdentifier(
identifier = currentIdentifier
)
} ?: uuid4().toString().let { trackingIdentifier: String ->
logger.i("$TAG Generating new Tracking Identifier value.")
userConfigRepository.setCurrentTrackingIdentifier(
newIdentifier = trackingIdentifier
)

AnalyticsIdentifierResult.NonExistingIdentifier(
identifier = trackingIdentifier
)
}
}
}
}
Loading
Loading