Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-08 14:13:07 +02:00
parent e726f26dcf
commit 06f50b0362
3 changed files with 80 additions and 56 deletions

View file

@ -3,7 +3,6 @@ package com.tangem.features.addressbook.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.domain.models.wallet.UserWalletId
private const val ADDRESS_BOOK_CATEGORY = "Address Book"
@ -24,85 +23,85 @@ sealed class AddressBookEvents(
data object AddressScreenOpened : AddressBookEvents(event = "Address Screen Opened")
class ContactListScreenOpened(
walletId: UserWalletId,
walletId: String,
source: Source,
contactsCount: Int,
) : AddressBookEvents(
event = "Contact List Screen Opened",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
SOURCE to source.value,
CONTACTS_COUNT to contactsCount.toString(),
),
) {
enum class Source(val value: String) {
Settings("settings"),
SendFlow("send_flow"),
Settings("Settings"),
SendFlow("Send Flow"),
}
}
class AddContactTapped(
walletId: UserWalletId,
walletId: String,
source: Source,
) : AddressBookEvents(
event = "Add Contact Tapped",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
SOURCE to source.value,
),
) {
enum class Source(val value: String) {
Settings("settings"),
SendSuccess("send_success"),
Settings("Settings"),
SendSuccess("Send Flow"),
}
}
class ContactSaved(
walletId: UserWalletId,
walletId: String,
contactId: String,
mode: Mode,
) : AddressBookEvents(
event = "Contact Saved",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
CONTACT_ID to contactId,
MODE to mode.value,
),
) {
enum class Mode(val value: String) {
Create("create"),
Edit("edit"),
Create("Create"),
Edit("Edit"),
}
}
class SaveErrorShown(
walletId: UserWalletId,
walletId: String,
contactId: String?,
errorType: ErrorType,
) : AddressBookEvents(
event = "Save Error Shown",
params = buildMap {
put(WALLET_ID, walletId.stringValue)
put(WALLET_ID, walletId)
contactId?.let { put(CONTACT_ID, it) }
put(ERROR_TYPE, errorType.value)
},
) {
enum class ErrorType(val value: String) {
Network("network"),
Server("server"),
Signing("signing"),
Network("Network"),
Server("Server"),
Signing("Signing"),
}
}
// endregion
// region Contact editing
class ContactScreenOpened(
walletId: UserWalletId,
walletId: String,
contactId: String,
) : AddressBookEvents(
event = "Contact Screen Opened",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
CONTACT_ID to contactId,
),
)
@ -110,30 +109,30 @@ sealed class AddressBookEvents(
// region Send flow
class SendFlowWidgetShown(
walletId: UserWalletId,
walletId: String,
) : AddressBookEvents(
event = "Send Flow Widget Shown",
params = mapOf(WALLET_ID to walletId.stringValue),
params = mapOf(WALLET_ID to walletId),
)
class ContactSelectedInSend(
walletId: UserWalletId,
walletId: String,
contactId: String,
) : AddressBookEvents(
event = "Contact Selected",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
CONTACT_ID to contactId,
),
)
class AddressSubstitutedInSend(
walletId: UserWalletId,
walletId: String,
contactId: String,
) : AddressBookEvents(
event = "Address Substituted In Send",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
CONTACT_ID to contactId,
),
)
@ -141,23 +140,23 @@ sealed class AddressBookEvents(
// region Input errors
class AddressInvalid(
walletId: UserWalletId,
walletId: String,
contactId: String?,
) : AddressBookEvents(
event = "Address Invalid",
params = buildMap {
put(WALLET_ID, walletId.stringValue)
put(WALLET_ID, walletId)
put(CONTACT_ID, contactId.orEmpty())
},
)
class DuplicateNameErrorShown(
walletId: UserWalletId,
walletId: String,
contactId: String?,
) : AddressBookEvents(
event = "Duplicate Name Error Shown",
params = buildMap {
put(WALLET_ID, walletId.stringValue)
put(WALLET_ID, walletId)
contactId?.let { put(CONTACT_ID, it) }
},
)
@ -183,23 +182,23 @@ sealed class AddressBookEvents(
// region Deletion
class AddressRemoved(
walletId: UserWalletId,
walletId: String,
contactId: String,
) : AddressBookEvents(
event = "Address Removed",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
CONTACT_ID to contactId,
),
)
class ContactDeleted(
walletId: UserWalletId,
walletId: String,
contactId: String,
) : AddressBookEvents(
event = "Contact Deleted",
params = mapOf(
WALLET_ID to walletId.stringValue,
WALLET_ID to walletId,
CONTACT_ID to contactId,
),
)

View file

@ -1,5 +1,7 @@
package com.tangem.features.addressbook.common
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.addressbook.error.AddressBookSyncError
import com.tangem.domain.addressbook.error.SaveContactError
@ -60,31 +62,31 @@ internal class AddressBookAnalyticsSender @Inject constructor(
override fun onAddressSubstitutedInSend(walletId: UserWalletId, contactId: String) {
analyticsEventHandler.send(
AddressBookEvents.AddressSubstitutedInSend(walletId = walletId, contactId = contactId),
AddressBookEvents.AddressSubstitutedInSend(walletId = walletId.hashForAnalytics(), contactId = contactId),
)
}
fun sendAddressInvalid(walletId: UserWalletId, contactId: String) {
analyticsEventHandler.send(
AddressBookEvents.AddressInvalid(walletId = walletId, contactId = contactId),
AddressBookEvents.AddressInvalid(walletId = walletId.hashForAnalytics(), contactId = contactId),
)
}
fun sendDuplicateNameErrorShown(walletId: UserWalletId, contactId: String?) {
analyticsEventHandler.send(
AddressBookEvents.DuplicateNameErrorShown(walletId = walletId, contactId = contactId),
AddressBookEvents.DuplicateNameErrorShown(walletId = walletId.hashForAnalytics(), contactId = contactId),
)
}
fun sendAddressRemoved(walletId: UserWalletId, contactId: String) {
analyticsEventHandler.send(
AddressBookEvents.AddressRemoved(walletId = walletId, contactId = contactId),
AddressBookEvents.AddressRemoved(walletId = walletId.hashForAnalytics(), contactId = contactId),
)
}
fun sendContactDeleted(walletId: UserWalletId, contactId: String) {
analyticsEventHandler.send(
AddressBookEvents.ContactDeleted(walletId = walletId, contactId = contactId),
AddressBookEvents.ContactDeleted(walletId = walletId.hashForAnalytics(), contactId = contactId),
)
}
@ -114,7 +116,7 @@ internal class AddressBookAnalyticsSender @Inject constructor(
fun sendContactSaved(walletId: UserWalletId, contactId: String, isEdit: Boolean) {
analyticsEventHandler.send(
AddressBookEvents.ContactSaved(
walletId = walletId,
walletId = walletId.hashForAnalytics(),
contactId = contactId,
mode = if (isEdit) {
AddressBookEvents.ContactSaved.Mode.Edit
@ -133,7 +135,7 @@ internal class AddressBookAnalyticsSender @Inject constructor(
val errorType = error.toErrorType() ?: return
analyticsEventHandler.send(
AddressBookEvents.SaveErrorShown(
walletId = walletId,
walletId = walletId.hashForAnalytics(),
contactId = contactId,
errorType = errorType,
),
@ -158,8 +160,12 @@ internal class AddressBookAnalyticsSender @Inject constructor(
analyticsEventHandler.send(AddressBookEvents.AddressScreenOpened)
}
private suspend fun selectedWalletId(): UserWalletId = userWalletsListRepository.selectedUserWallet
private suspend fun selectedWalletId(): String = userWalletsListRepository.selectedUserWallet
.filterNotNull()
.first()
.walletId
.hashForAnalytics()
/** Analytics must never receive a raw wallet id — send its SHA-256 (uppercase hex) instead. */
private fun UserWalletId.hashForAnalytics(): String = value.calculateSha256().toHexString()
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.addressbook.common
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.addressbook.error.AddressBookSyncError
import com.tangem.domain.addressbook.error.SaveContactError
@ -56,7 +58,7 @@ internal class AddressBookAnalyticsSenderTest {
// Assert
val expected = AddressBookEvents.ContactListScreenOpened(
walletId = EXPECTED_WALLET_ID,
walletId = EXPECTED_WALLET_ID_HASH,
source = model.source,
contactsCount = model.contactsCount,
)
@ -72,7 +74,7 @@ internal class AddressBookAnalyticsSenderTest {
// Assert
val expected = AddContactTapped(
walletId = EXPECTED_WALLET_ID,
walletId = EXPECTED_WALLET_ID_HASH,
source = model.expectedSource,
)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
@ -86,7 +88,7 @@ internal class AddressBookAnalyticsSenderTest {
// Assert
val expected = ContactSaved(
walletId = EXPECTED_WALLET_ID,
walletId = EXPECTED_WALLET_ID_HASH,
contactId = CONTACT_ID,
mode = model.expectedMode,
)
@ -105,7 +107,7 @@ internal class AddressBookAnalyticsSenderTest {
verify(exactly = 0) { analyticsEventHandler.send(any()) }
} else {
val expected = AddressBookEvents.SaveErrorShown(
walletId = EXPECTED_WALLET_ID,
walletId = EXPECTED_WALLET_ID_HASH,
contactId = CONTACT_ID,
errorType = expectedType,
)
@ -138,7 +140,7 @@ internal class AddressBookAnalyticsSenderTest {
advanceUntilIdle()
// Assert
val expected = AddressBookEvents.ContactScreenOpened(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
val expected = AddressBookEvents.ContactScreenOpened(walletId = EXPECTED_WALLET_ID_HASH, contactId = CONTACT_ID)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -149,7 +151,7 @@ internal class AddressBookAnalyticsSenderTest {
advanceUntilIdle()
// Assert
val expected = AddressBookEvents.SendFlowWidgetShown(walletId = EXPECTED_WALLET_ID)
val expected = AddressBookEvents.SendFlowWidgetShown(walletId = EXPECTED_WALLET_ID_HASH)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -160,7 +162,10 @@ internal class AddressBookAnalyticsSenderTest {
advanceUntilIdle()
// Assert
val expected = AddressBookEvents.ContactSelectedInSend(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
val expected = AddressBookEvents.ContactSelectedInSend(
walletId = EXPECTED_WALLET_ID_HASH,
contactId = CONTACT_ID,
)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -170,7 +175,8 @@ internal class AddressBookAnalyticsSenderTest {
sender.onAddressSubstitutedInSend(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
// Assert
val expected = AddressBookEvents.AddressSubstitutedInSend(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
val expected =
AddressBookEvents.AddressSubstitutedInSend(walletId = EXPECTED_WALLET_ID_HASH, contactId = CONTACT_ID)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -181,7 +187,7 @@ internal class AddressBookAnalyticsSenderTest {
sender.sendAddressInvalid(walletId = EXPECTED_WALLET_ID, contactId = contactId)
// Assert
val expected = AddressBookEvents.AddressInvalid(walletId = EXPECTED_WALLET_ID, contactId = contactId)
val expected = AddressBookEvents.AddressInvalid(walletId = EXPECTED_WALLET_ID_HASH, contactId = contactId)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -192,7 +198,10 @@ internal class AddressBookAnalyticsSenderTest {
sender.sendDuplicateNameErrorShown(walletId = EXPECTED_WALLET_ID, contactId = contactId)
// Assert
val expected = AddressBookEvents.DuplicateNameErrorShown(walletId = EXPECTED_WALLET_ID, contactId = contactId)
val expected = AddressBookEvents.DuplicateNameErrorShown(
walletId = EXPECTED_WALLET_ID_HASH,
contactId = contactId,
)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -202,7 +211,7 @@ internal class AddressBookAnalyticsSenderTest {
sender.sendAddressRemoved(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
// Assert
val expected = AddressBookEvents.AddressRemoved(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
val expected = AddressBookEvents.AddressRemoved(walletId = EXPECTED_WALLET_ID_HASH, contactId = CONTACT_ID)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -212,7 +221,7 @@ internal class AddressBookAnalyticsSenderTest {
sender.sendContactDeleted(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
// Assert
val expected = AddressBookEvents.ContactDeleted(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID)
val expected = AddressBookEvents.ContactDeleted(walletId = EXPECTED_WALLET_ID_HASH, contactId = CONTACT_ID)
verify(exactly = 1) { analyticsEventHandler.send(expected) }
}
@ -241,9 +250,15 @@ internal class AddressBookAnalyticsSenderTest {
private fun provideSaveErrorModels() = listOf(
SaveErrorModel(error = SaveContactError.Signing(mockk()), expectedType = ErrorType.Signing),
SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Network), expectedType = ErrorType.Network),
SaveErrorModel(
error = SaveContactError.Backend(AddressBookSyncError.Network),
expectedType = ErrorType.Network,
),
// 412
SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Conflict), expectedType = ErrorType.Server),
SaveErrorModel(
error = SaveContactError.Backend(AddressBookSyncError.Conflict),
expectedType = ErrorType.Server,
),
// 5xx / unmapped
SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Unknown), expectedType = ErrorType.Server),
SaveErrorModel(
@ -262,6 +277,10 @@ internal class AddressBookAnalyticsSenderTest {
private companion object {
val EXPECTED_WALLET_ID = UserWalletId("0011223344")
// Analytics receives the SHA-256 (uppercase hex) of the wallet id, never the raw value.
val EXPECTED_WALLET_ID_HASH = EXPECTED_WALLET_ID.value.calculateSha256().toHexString()
const val CONTACT_ID = "contact-42"
}
}