Skip to content

Commit

Permalink
feat: Add Anonymous Analytics User Profile EPIC - (WPB-8978) (#2887)
Browse files Browse the repository at this point in the history
* feat: add analytics user profile - Part 1 (WPB-8978) (#2868)

* feat: add method to set/insert tracking identifier into user metadata table

* feat: add MessageContent for DataTransfer with TrackingIdentifier

* feat: add DataTransferEventHandler to handle new data transfer events and set tracking identifier to user config metadata

* chore: remove needless blank line

* feat: add analytics user profile - Part 2 (WPB-8978) (#2877)

* feat: add method to set/insert tracking identifier into user metadata table

* feat: add MessageContent for DataTransfer with TrackingIdentifier

* feat: add DataTransferEventHandler to handle new data transfer events and set tracking identifier to user config metadata

* chore: remove needless blank line

* feat: add get for current tracking identifier and get and set for previous tracking identifier

* feat: add usage and logic handling for current and previous tracking identifiers

* feat: add tests

* feat: add better handling of receiving already existing tracking id and tests

* feat: add new analytics logger to kalium logger

* feat: add usecase to observe analytics tracking identifier

* feat: add tests for observeAnalyticsTrackingIdentifierStatus

* feat: add none AnalyticsIdentifierResult to be used in AR and remove abstract attribute

* feat: add proper mapping to observer result and verification on Either Left is tracking identifier already exists

* feat: add tests for observer with new logic

* chore: add user scope logger

* chore: add `as` instantiation of right value for usecase result

* feat: move AnalyticsIdentifierResult to new data module and update its usages

* feat: add usecase to delete previous tracking identifier

* chore: add missing imports

* test: add test for DeletePreviousTrackingIdentifierUseCase

* chore: remove delete previous tracking identifier use case

* feat: add extra sealed interface for better handling of analytics identifier result

* feat: add AnalyticsIdentifierManager to handle migration complete and propagating tracking identifier

* chore: add missing extension from new sealed interface

* test: add tests for AnalyticsIdentifierManager

* chore: adjust detekt

* chore: remove unused import

* chore: rename user config current tracking identifier

* chore: add docs
  • Loading branch information
alexandreferris authored Jul 22, 2024
1 parent afefda7 commit f534d36
Show file tree
Hide file tree
Showing 20 changed files with 966 additions and 6 deletions.
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 {

/**
* 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 @@ -180,7 +180,7 @@ sealed interface Message {
return "${toLogMap().toJsonElement()}"
}

@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun toLogMap(): Map<String, Any?> {
val typeKey = "type"

Expand Down Expand Up @@ -240,6 +240,11 @@ sealed interface Message {
is MessageContent.ButtonActionConfirmation -> mutableMapOf(
typeKey to "buttonActionConfirmation"
)

is MessageContent.DataTransfer -> mutableMapOf(
typeKey to "dataTransfer",
"content" to content.toLogMap(),
)
}

val standardProperties = mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ sealed interface MessageContent {
)
}

data class DataTransfer(
var trackingIdentifier: TrackingIdentifier? = null
) : Signaling {
fun toLogMap(): Map<String, Any> = mapOf(
"identifier" to (trackingIdentifier?.identifier?.obfuscateId() ?: "null")
)

data class TrackingIdentifier(
val identifier: String
)
}

data class DeleteMessage(val messageId: String) : Signaling

data class TextEdited(
Expand Down Expand Up @@ -440,6 +452,7 @@ fun MessageContent?.getType() = when (this) {
is MessageContent.LegalHold.ForConversation.Enabled -> "LegalHold.ForConversation.Enabled"
is MessageContent.LegalHold.ForMembers.Disabled -> "LegalHold.ForMembers.Disabled"
is MessageContent.LegalHold.ForMembers.Enabled -> "LegalHold.ForMembers.Enabled"
is MessageContent.DataTransfer -> "DataTransfer"
null -> "null"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ inline fun MessageContent.FromProto.typeDescription(): String = when (this) {
is MessageContent.Reaction -> "Reaction"
is MessageContent.Receipt -> "Receipt"
is MessageContent.TextEdited -> "TextEdited"
is MessageContent.DataTransfer -> "DataTransfer"
}
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,6 +136,12 @@ interface UserConfigRepository {
suspend fun clearE2EISettings()
fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean)
fun getShouldFetchE2EITrustAnchor(): Boolean
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?
suspend fun deletePreviousTrackingIdentifier()
}

@Suppress("TooManyFunctions")
Expand Down Expand Up @@ -495,4 +501,31 @@ internal class UserConfigDataSource internal constructor(
}

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

override suspend fun setCurrentTrackingIdentifier(newIdentifier: String) {
wrapStorageRequest {
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 @@ -126,6 +126,7 @@ internal class PersistMessageUseCaseImpl(
is MessageContent.LegalHold -> false
is MessageContent.MemberChange.RemovedFromTeam -> false
is MessageContent.TeamMemberRemoved -> false
is MessageContent.DataTransfer -> false
}

@Suppress("ComplexMethod")
Expand Down Expand Up @@ -179,6 +180,7 @@ internal class PersistMessageUseCaseImpl(
is MessageContent.ConversationStartedUnverifiedWarning,
is MessageContent.LegalHold,
is MessageContent.MemberChange.RemovedFromTeam,
is MessageContent.TeamMemberRemoved -> false
is MessageContent.TeamMemberRemoved,
is MessageContent.DataTransfer -> false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ interface ProtoContentMapper {
fun decodeFromProtobuf(encodedContent: PlainMessageBlob): ProtoContent
}

@Suppress("TooManyFunctions", "LongParameterList")
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
class ProtoContentMapperImpl(
private val assetMapper: AssetMapper = MapperProvider.assetMapper(),
private val availabilityMapper: AvailabilityStatusMapper = MapperProvider.availabilityStatusMapper(),
Expand Down Expand Up @@ -138,6 +138,8 @@ class ProtoContentMapperImpl(

is MessageContent.ButtonActionConfirmation -> packButtonActionConfirmation(readableContent)
is MessageContent.Location -> packLocation(readableContent, expectsReadConfirmation, legalHoldStatus)

is MessageContent.DataTransfer -> TODO("Analytics: Not yet implemented")
}
}

Expand Down Expand Up @@ -257,7 +259,8 @@ class ProtoContentMapperImpl(
is MessageContent.Composite,
is MessageContent.ButtonAction,
is MessageContent.ButtonActionConfirmation,
is MessageContent.TextEdited -> throw IllegalArgumentException(
is MessageContent.TextEdited,
is MessageContent.DataTransfer -> throw IllegalArgumentException(
"Unexpected message content type: ${readableContent.getType()}"
)
}
Expand Down Expand Up @@ -352,7 +355,7 @@ class ProtoContentMapperImpl(
is GenericMessage.Content.Cleared -> unpackCleared(protoContent)
is GenericMessage.Content.ClientAction -> MessageContent.ClientAction
is GenericMessage.Content.Confirmation -> unpackReceipt(protoContent)
is GenericMessage.Content.DataTransfer -> MessageContent.Ignored
is GenericMessage.Content.DataTransfer -> unpackDataTransfer(protoContent)
is GenericMessage.Content.Deleted -> MessageContent.DeleteMessage(protoContent.value.messageId)
is GenericMessage.Content.Edited -> unpackEdited(protoContent, typeName, encodedContent, genericMessage)
is GenericMessage.Content.Ephemeral -> unpackEphemeral(protoContent)
Expand Down Expand Up @@ -536,6 +539,14 @@ class ProtoContentMapperImpl(
conversationId = protoContent.value.qualifiedConversationId?.let { idMapper.fromProtoModel(it) }
)

private fun unpackDataTransfer(protoContent: GenericMessage.Content.DataTransfer) = MessageContent.DataTransfer(
trackingIdentifier = protoContent.value.trackingIdentifier?.let { trackingIdentifier ->
MessageContent.DataTransfer.TrackingIdentifier(
identifier = trackingIdentifier.identifier
)
}
)

private fun packCleared(readableContent: MessageContent.Cleared) = GenericMessage.Content.Cleared(
Cleared(
conversationId = readableContent.conversationId.value,
Expand Down
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 @@ -411,6 +413,8 @@ import com.wire.kalium.logic.sync.receiver.handler.CodeDeletedHandler
import com.wire.kalium.logic.sync.receiver.handler.CodeDeletedHandlerImpl
import com.wire.kalium.logic.sync.receiver.handler.CodeUpdateHandlerImpl
import com.wire.kalium.logic.sync.receiver.handler.CodeUpdatedHandler
import com.wire.kalium.logic.sync.receiver.handler.DataTransferEventHandler
import com.wire.kalium.logic.sync.receiver.handler.DataTransferEventHandlerImpl
import com.wire.kalium.logic.sync.receiver.handler.DeleteForMeHandlerImpl
import com.wire.kalium.logic.sync.receiver.handler.DeleteMessageHandlerImpl
import com.wire.kalium.logic.sync.receiver.handler.LastReadContentHandlerImpl
Expand Down Expand Up @@ -1304,6 +1308,13 @@ class UserSessionScope internal constructor(
private val buttonActionConfirmationHandler: ButtonActionConfirmationHandler
get() = ButtonActionConfirmationHandlerImpl(compositeMessageRepository, messageMetadataRepository)

private val dataTransferEventHandler: DataTransferEventHandler
get() = DataTransferEventHandlerImpl(
userId,
userConfigRepository,
userScopedLogger
)

private val applicationMessageHandler: ApplicationMessageHandler
get() = ApplicationMessageHandlerImpl(
userRepository,
Expand All @@ -1329,6 +1340,7 @@ class UserSessionScope internal constructor(
messageEncoder,
receiptMessageHandler,
buttonActionConfirmationHandler,
dataTransferEventHandler,
userId
)

Expand Down Expand Up @@ -1466,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
Loading

0 comments on commit f534d36

Please sign in to comment.