diff --git a/app/build.gradle b/app/build.gradle
index f1fa3b08ef..70b6edcebd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -122,12 +122,12 @@ dependencies {
// Firebase
implementation platform('com.google.firebase:firebase-bom:26.0.0')
- implementation 'com.google.firebase:firebase-crashlytics'
- implementation 'com.google.firebase:firebase-config-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
+ implementation 'com.google.firebase:firebase-crashlytics-ktx'
- // AppsFlyer
+ // Analytics
implementation 'com.appsflyer:af-android-sdk:6.5.1'
+ implementation 'com.amplitude:android-sdk:2.36.1'
// WalletConnect
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4422e037f9..874783a15d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index 8fa11bf17a..c0d6e566b4 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -5,16 +5,16 @@ import android.content.Context
import android.content.pm.PackageManager
import coil.ImageLoader
import coil.ImageLoaderFactory
-import com.appsflyer.AppsFlyerLib
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
+import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.AssetReader
-import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
+import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandlerBuilder
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
@@ -97,15 +97,19 @@ class TapApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
- initAppsFlyer(this, config)
+ initAnalytics(this, config)
initFeedbackManager(this, preferencesStorage, config)
}
- private fun initAppsFlyer(context: Context, config: Config) {
- AppsFlyerLib.getInstance().init(config.appsFlyerDevKey, null, context)
- AppsFlyerLib.getInstance().start(context)
- val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(context)
- store.dispatch(GlobalAction.SetAnanlyticHandlers(analyticsHandler))
+ private fun initAnalytics(application: Application, config: Config) {
+ val globalAnalyticsHandler = GlobalAnalyticsEventHandlerBuilder(
+ application = application,
+ config = config,
+ isDebug = BuildConfig.DEBUG,
+ logConfig = LogConfig.analyticsHandlers,
+ jsonConverter = MoshiJsonConverter.INSTANCE,
+ ).default()
+ store.dispatch(GlobalAction.SetGlobalAnalyticsHandler(globalAnalyticsHandler))
}
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt
new file mode 100644
index 0000000000..0865d2aa81
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt
@@ -0,0 +1,26 @@
+package com.tangem.tap.common.analytics
+
+import com.tangem.common.json.MoshiJsonConverter
+import com.tangem.tap.common.analytics.api.ErrorEventLogger
+import com.tangem.tap.common.analytics.api.EventLogger
+import timber.log.Timber
+
+class AnalyticsEventsLogger(
+ private val name: String,
+ private val jsonConverter: MoshiJsonConverter,
+) : EventLogger, ErrorEventLogger {
+
+ override fun logEvent(event: String, params: Map) {
+ Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
+ }
+
+ override fun logErrorEvent(error: Throwable, params: Map) {
+ Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params)))
+ }
+}
+
+private data class PrintEventModel(
+ val client: String,
+ val event: String,
+ val params: Map,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt
deleted file mode 100644
index 424f06d9ce..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.tangem.tap.common.analytics
-
-import com.shopify.buy3.Storefront
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
-import com.tangem.tap.common.extensions.filterNotNull
-
-
-abstract class AnalyticsHandler {
- abstract fun triggerEvent(
- event: AnalyticsEvent,
- card: Card? = null,
- blockchain: String? = null,
- params: Map = emptyMap()
- )
-
- abstract fun triggerEvent(
- event: String,
- params: Map
- )
-
- abstract fun logCardSdkError(
- error: TangemSdkError,
- actionToLog: Analytics.ActionToLog,
- parameters: Map? = null,
- card: Card? = null
- )
-
- abstract fun logError(
- error: Throwable,
- params: Map = emptyMap()
- )
-
- protected fun prepareParams(card: Card?, blockchain: String? = null): Map {
- return mapOf(
- AnalyticsParam.FIRMWARE.param to card?.firmwareVersion?.stringValue,
- AnalyticsParam.BATCH_ID.param to card?.batchId,
- AnalyticsParam.BLOCKCHAIN.param to blockchain,
- ).filterNotNull()
- }
-
- fun logWcEvent(event: Analytics.WcAnalyticsEvent) {
- when (event) {
- is Analytics.WcAnalyticsEvent.Action -> {
- triggerEvent(
- event = AnalyticsEvent.WC_SUCCESS_RESPONSE,
- params = mapOf(
- AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action.name
- )
- )
- }
- is Analytics.WcAnalyticsEvent.Error -> {
- val params = mapOf(
- AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action?.name,
- AnalyticsParam.ERROR_DESCRIPTION.param to event.error.message
- )
- .filterNotNull()
- logError(event.error, params)
- }
- is Analytics.WcAnalyticsEvent.InvalidRequest ->
- triggerEvent(
- event = AnalyticsEvent.WC_INVALID_REQUEST,
- params = mapOf(
- AnalyticsParam.WALLET_CONNECT_REQUEST.param to event.json
- ).filterNotNull()
- )
- is Analytics.WcAnalyticsEvent.Session -> {
- val analyticsEvent = when (event.event) {
- Analytics.WcSessionEvent.Disconnect -> AnalyticsEvent.WC_SESSION_DISCONNECTED
- Analytics.WcSessionEvent.Connect -> AnalyticsEvent.WC_NEW_SESSION
- }
- triggerEvent(
- event = analyticsEvent,
- params = mapOf(
- AnalyticsParam.WALLET_CONNECT_DAPP_URL.param to event.url
- ).filterNotNull()
- )
- }
- }
- }
-
- fun logShopifyOrder(order: Storefront.Order) {
- triggerEvent(getOrderEvent(), getOrderParams(order))
- }
-
- protected abstract fun getOrderEvent(): String
-
- protected abstract fun getOrderParams(order: Storefront.Order): Map
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt
new file mode 100644
index 0000000000..6fd6fc7dc8
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt
@@ -0,0 +1,3 @@
+package com.tangem.tap.common.analytics
+
+
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt
deleted file mode 100644
index 16fa1644ab..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.tangem.tap.common.analytics
-
-import android.content.Context
-import com.appsflyer.AFInAppEventParameterName
-import com.appsflyer.AFInAppEventType
-import com.appsflyer.AppsFlyerLib
-import com.shopify.buy3.Storefront
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
-
-class AppsFlyerAnalyticsHandler(val context: Context): AnalyticsHandler() {
-
- override fun triggerEvent(
- event: AnalyticsEvent,
- card: Card?,
- blockchain: String?,
- params: Map
- ) {
- AppsFlyerLib.getInstance().logEvent(context, event.event, prepareParams(card, blockchain))
- }
-
- override fun logCardSdkError(
- error: TangemSdkError,
- actionToLog: Analytics.ActionToLog,
- parameters: Map?,
- card: Card?
- ) {
- //TODO
- }
-
- override fun logError(error: Throwable, params: Map) {
- //TODO
- }
-
- override fun getOrderEvent(): String = AFInAppEventType.PURCHASE
-
- override fun getOrderParams(order: Storefront.Order): Map {
-
- val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
-
- val discountCode =
- (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
-
- val discountParams = if (discountCode != null ){
- mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode)
- } else {
- mapOf()
- }
-
- return mapOf(
- AFInAppEventParameterName.CONTENT_ID to sku,
- AFInAppEventParameterName.REVENUE to order.totalPriceV2.amount,
- AFInAppEventParameterName.CURRENCY to order.currencyCode.name
- ) + discountParams
- }
-
- override fun triggerEvent(event: String, params: Map) {
- AppsFlyerLib.getInstance().logEvent(context, event, params)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt
deleted file mode 100644
index 4e0371206a..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.tangem.tap.common.analytics
-
-import android.os.Bundle
-import androidx.core.os.bundleOf
-import com.google.firebase.analytics.FirebaseAnalytics
-import com.google.firebase.analytics.ktx.analytics
-import com.google.firebase.crashlytics.FirebaseCrashlytics
-import com.google.firebase.ktx.Firebase
-import com.shopify.buy3.Storefront
-import com.tangem.common.card.Card
-import com.tangem.common.core.TangemSdkError
-
-object FirebaseAnalyticsHandler : AnalyticsHandler() {
- override fun triggerEvent(
- event: AnalyticsEvent,
- card: Card?,
- blockchain: String?,
- params: Map
- ) {
- val fullParams = prepareParams(card, blockchain) + params
- Firebase.analytics.logEvent(event.event, fullParams.toBundle())
- }
-
- fun logException(name: String, throwable: Throwable) {
- Firebase.analytics.logEvent(name, bundleOf(
- "message" to (throwable.message ?: "none"),
- "cause_message" to (throwable.cause?.message ?: "none"),
- "stack_trace" to throwable.stackTraceToString()
- ))
- }
-
- private fun Map.toBundle(): Bundle {
- return bundleOf(*this.toList().toTypedArray())
- }
-
- override fun logCardSdkError(
- error: TangemSdkError,
- actionToLog: Analytics.ActionToLog,
- parameters: Map?,
- card: Card?,
- ) {
- if (error is TangemSdkError.UserCancelled) return
-
- val params = parameters?.toMutableMap() ?: mutableMapOf()
- if (card != null) params + prepareParams(card)
- params[AnalyticsParam.ACTION] = actionToLog.key
- params[AnalyticsParam.ERROR_CODE] = error.code.toString()
- params[AnalyticsParam.ERROR_DESCRIPTION] = error.javaClass.simpleName
- params[AnalyticsParam.ERROR_KEY] = "TangemSdkError"
-
- params.forEach {
- FirebaseCrashlytics.getInstance().setCustomKey(it.key.param, it.value)
- }
- val cardError = TangemSdk.map(error)
- FirebaseCrashlytics.getInstance().recordException(cardError)
- }
-
- override fun logError(error: Throwable, params: Map) {
- params.forEach { (key, value) -> FirebaseCrashlytics.getInstance().setCustomKey(key, value) }
- FirebaseCrashlytics.getInstance().recordException(error)
- }
-
- override fun getOrderEvent(): String = FirebaseAnalytics.Event.PURCHASE
-
- override fun getOrderParams(order: Storefront.Order): Map {
-
- val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
-
- val discountCode =
- (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
-
- val discountParams = if (discountCode != null ){
- mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode)
- } else {
- mapOf()
- }
-
- return mapOf(
- FirebaseAnalytics.Param.ITEM_ID to sku,
- FirebaseAnalytics.Param.VALUE to order.totalPriceV2.amount,
- FirebaseAnalytics.Param.CURRENCY to order.currencyCode.name
- ) + discountParams
- }
-
- override fun triggerEvent(event: String, params: Map) {
- Firebase.analytics.logEvent(event, params.toBundle())
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt
new file mode 100644
index 0000000000..8e2b439e08
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt
@@ -0,0 +1,79 @@
+package com.tangem.tap.common.analytics
+
+import android.app.Application
+import com.tangem.common.json.MoshiJsonConverter
+import com.tangem.domain.common.AnalyticsHandlersLogConfig
+import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
+import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
+import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeClient
+import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeLogClient
+import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
+import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerClient
+import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerLogClient
+import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
+import com.tangem.tap.common.analytics.handlers.firebase.FirebaseClient
+import com.tangem.tap.common.analytics.handlers.firebase.FirebaseLogClient
+import com.tangem.tap.domain.configurable.config.Config
+
+/**
+[REDACTED_AUTHOR]
+ */
+class GlobalAnalyticsEventHandlerBuilder(
+ val application: Application,
+ val config: Config,
+ val isDebug: Boolean,
+ val logConfig: AnalyticsHandlersLogConfig,
+ val jsonConverter: MoshiJsonConverter,
+) {
+
+ private val analyticHandlers = mutableListOf()
+
+ fun build(): GlobalAnalyticsEventHandler {
+ return GlobalAnalyticsHandler(analyticHandlers.toList())
+ }
+
+ fun default(): GlobalAnalyticsEventHandler {
+ addFirebaseAnalytics()
+ addAppsFlyer()
+ addAmplitude()
+
+ return GlobalAnalyticsHandler(analyticHandlers.toList())
+ }
+
+ fun addFirebaseAnalytics(): GlobalAnalyticsEventHandlerBuilder {
+ if (isDebug) {
+ if (logConfig.firebase) FirebaseLogClient(createLogger("FirebaseAnalyticsHandler")) else null
+ } else {
+ FirebaseClient()
+ }?.let {
+ analyticHandlers.add(FirebaseAnalyticsHandler(it))
+ }
+ return this
+ }
+
+ fun addAppsFlyer(): GlobalAnalyticsEventHandlerBuilder {
+ if (isDebug) {
+ if (logConfig.appsFlyer) AppsFlyerLogClient(createLogger("AppsFlyerAnalyticsHandler")) else null
+ } else {
+ AppsFlyerClient(application, config.appsFlyerDevKey)
+ }?.let {
+ analyticHandlers.add(AppsFlyerAnalyticsHandler(it))
+ }
+ return this
+ }
+
+ fun addAmplitude(): GlobalAnalyticsEventHandlerBuilder {
+ if (isDebug) {
+ if (logConfig.firebase) AmplitudeLogClient(createLogger("AmplitudeAnalyticsHandler")) else null
+ } else {
+ AmplitudeClient(application, config.amplitudeApiKey)
+ }?.let {
+ analyticHandlers.add(AmplitudeAnalyticsHandler(it))
+ }
+ return this
+ }
+
+ private fun createLogger(name: String): AnalyticsEventsLogger {
+ return AnalyticsEventsLogger(name, jsonConverter)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt
index 197f396621..660664c1f7 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt
@@ -1,53 +1,109 @@
package com.tangem.tap.common.analytics
-import android.content.Context
import com.shopify.buy3.Storefront
+import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
+import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
+import com.tangem.tap.common.analytics.api.BlockchainSdkErrorEventHandler
+import com.tangem.tap.common.analytics.api.CardSdkErrorEventHandler
+import com.tangem.tap.common.analytics.api.ErrorEventHandler
+import com.tangem.tap.common.analytics.api.ErrorEventLogger
+import com.tangem.tap.common.analytics.api.SdkErrorEventHandler
+import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
+import com.tangem.tap.common.extensions.filterNotNull
-class GlobalAnalyticsHandler(val analyticsHandlers: List) :
- AnalyticsHandler() {
- override fun triggerEvent(
+interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
+ ErrorEventHandler,
+ SdkErrorEventHandler,
+ ShopifyOrderEventHandler
+
+class GlobalAnalyticsHandler(
+ private val analyticsHandlers: List,
+) : GlobalAnalyticsEventHandler {
+
+ override fun handleEvent(event: String, params: Map) {
+ analyticsHandlers.forEach { it.handleEvent(event, params) }
+ }
+
+ override fun handleAnalyticsEvent(
event: AnalyticsEvent,
+ params: Map,
card: Card?,
blockchain: String?,
- params: Map
) {
- analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain, params) }
+ analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) }
}
- override fun triggerEvent(event: String, params: Map) {
- analyticsHandlers.forEach { it.triggerEvent(event, params) }
+ override fun handleErrorEvent(error: Throwable, params: Map) {
+ analyticsHandlers.filterIsInstance().forEach {
+ it.logErrorEvent(error, params)
+ }
}
- override fun logCardSdkError(
+ override fun handleCardSdkErrorEvent(
error: TangemSdkError,
- actionToLog: Analytics.ActionToLog,
- parameters: Map?,
- card: Card?
+ action: Analytics.ActionToLog,
+ params: Map,
+ card: Card?,
) {
- analyticsHandlers.forEach { it.logCardSdkError(error, actionToLog, parameters, card) }
+ analyticsHandlers.filterIsInstance().forEach {
+ it.handleCardSdkErrorEvent(error, action, params, card)
+ }
}
- override fun logError(error: Throwable, params: Map) {
- analyticsHandlers.forEach { it.logError(error, params) }
+ override fun handleBlockchainSdkErrorEvent(
+ error: BlockchainError,
+ action: Analytics.ActionToLog,
+ params: Map,
+ card: Card?,
+ ) {
+ analyticsHandlers.filterIsInstance().forEach {
+ it.handleBlockchainSdkErrorEvent(error, action, params, card)
+ }
}
- override fun getOrderEvent(): String {
- return ""
+ override fun handleShopifyOrderEvent(order: Storefront.Order) {
+ analyticsHandlers.filterIsInstance().forEach {
+ it.handleShopifyOrderEvent(order)
+ }
}
+}
- override fun getOrderParams(order: Storefront.Order): Map {
- return emptyMap()
- }
-
- companion object {
- fun createDefaultAnalyticHandlers(context: Context): GlobalAnalyticsHandler {
- return GlobalAnalyticsHandler(
- listOf(
- FirebaseAnalyticsHandler,
- AppsFlyerAnalyticsHandler(context)
- )
+fun GlobalAnalyticsEventHandler.logWcEvent(event: Analytics.WcAnalyticsEvent) {
+ when (event) {
+ is Analytics.WcAnalyticsEvent.Action -> {
+ handleAnalyticsEvent(
+ event = AnalyticsEvent.WC_SUCCESS_RESPONSE,
+ params = mapOf(
+ AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action.name,
+ ),
+ )
+ }
+ is Analytics.WcAnalyticsEvent.Error -> {
+ val params = mapOf(
+ AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action?.name,
+ AnalyticsParam.ERROR_DESCRIPTION.param to event.error.message,
+ ).filterNotNull()
+ handleErrorEvent(event.error, params)
+ }
+ is Analytics.WcAnalyticsEvent.InvalidRequest ->
+ handleAnalyticsEvent(
+ event = AnalyticsEvent.WC_INVALID_REQUEST,
+ params = mapOf(
+ AnalyticsParam.WALLET_CONNECT_REQUEST.param to event.json,
+ ).filterNotNull(),
+ )
+ is Analytics.WcAnalyticsEvent.Session -> {
+ val analyticsEvent = when (event.event) {
+ Analytics.WcSessionEvent.Disconnect -> AnalyticsEvent.WC_SESSION_DISCONNECTED
+ Analytics.WcSessionEvent.Connect -> AnalyticsEvent.WC_NEW_SESSION
+ }
+ handleAnalyticsEvent(
+ event = analyticsEvent,
+ params = mapOf(
+ AnalyticsParam.WALLET_CONNECT_DAPP_URL.param to event.url,
+ ).filterNotNull(),
)
}
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/ClientApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/ClientApi.kt
new file mode 100644
index 0000000000..3b5b0720f2
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/api/ClientApi.kt
@@ -0,0 +1,18 @@
+package com.tangem.tap.common.analytics.api
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface EventLogger {
+ fun logEvent(
+ event: String,
+ params: Map = emptyMap(),
+ )
+}
+
+interface ErrorEventLogger {
+ fun logErrorEvent(
+ error: Throwable,
+ params: Map = emptyMap(),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt
new file mode 100644
index 0000000000..00cb845770
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt
@@ -0,0 +1,68 @@
+package com.tangem.tap.common.analytics.api
+
+import com.shopify.buy3.Storefront
+import com.tangem.blockchain.common.BlockchainError
+import com.tangem.common.card.Card
+import com.tangem.common.core.TangemSdkError
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.AnalyticsEvent
+import com.tangem.tap.common.analytics.AnalyticsParam
+import com.tangem.tap.common.extensions.filterNotNull
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface AnalyticsEventHandler {
+ fun handleEvent(
+ event: String,
+ params: Map = emptyMap(),
+ )
+
+ fun handleAnalyticsEvent(
+ event: AnalyticsEvent,
+ params: Map = emptyMap(),
+ card: Card? = null,
+ blockchain: String? = null,
+ )
+
+ fun prepareParams(
+ card: Card?,
+ blockchain: String? = null,
+ params: Map = emptyMap(),
+ ): Map = mapOf(
+ AnalyticsParam.FIRMWARE.param to card?.firmwareVersion?.stringValue,
+ AnalyticsParam.BATCH_ID.param to card?.batchId,
+ AnalyticsParam.BLOCKCHAIN.param to blockchain,
+ ).filterNotNull() + params
+}
+
+interface ErrorEventHandler {
+ fun handleErrorEvent(
+ error: Throwable,
+ params: Map = emptyMap(),
+ )
+}
+
+interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler
+
+interface CardSdkErrorEventHandler {
+ fun handleCardSdkErrorEvent(
+ error: TangemSdkError,
+ action: Analytics.ActionToLog,
+ params: Map = emptyMap(),
+ card: Card? = null,
+ )
+}
+
+interface BlockchainSdkErrorEventHandler {
+ fun handleBlockchainSdkErrorEvent(
+ error: BlockchainError,
+ action: Analytics.ActionToLog,
+ params: Map = mapOf(),
+ card: Card? = null,
+ )
+}
+
+interface ShopifyOrderEventHandler {
+ fun handleShopifyOrderEvent(order: Storefront.Order)
+}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt
new file mode 100644
index 0000000000..470933771a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt
@@ -0,0 +1,88 @@
+package com.tangem.tap.common.analytics.converters
+
+import com.tangem.blockchain.common.BlockchainError
+import com.tangem.blockchain.common.BlockchainSdkError
+import com.tangem.common.Converter
+import com.tangem.common.core.TangemSdkError
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.AnalyticsParam
+import com.tangem.tap.common.analytics.TangemSdk
+import com.tangem.tap.features.demo.DemoTransactionSender
+
+/**
+[REDACTED_AUTHOR]
+ */
+internal data class ErrorEvent(
+ val throwable: Throwable,
+ val params: Map,
+)
+
+internal class CardSdkErrorConverter : Converter {
+
+ override fun convert(value: Model): ErrorEvent? {
+ if (value.error is TangemSdkError.UserCancelled) return null
+
+ val mutableParams = value.params.convertToParamValue().toMutableMap().apply {
+ putAll(value.cardParams)
+ }
+ mutableParams[AnalyticsParam.ACTION.param] = value.action.key
+ mutableParams[AnalyticsParam.ERROR_CODE.param] = value.error.code.toString()
+ mutableParams[AnalyticsParam.ERROR_DESCRIPTION.param] = value.error.javaClass.simpleName
+ mutableParams[AnalyticsParam.ERROR_KEY.param] = "TangemSdkError"
+
+ return ErrorEvent(TangemSdk.map(value.error), mutableParams)
+ }
+
+ internal data class Model(
+ val error: TangemSdkError,
+ val action: Analytics.ActionToLog,
+ val params: Map,
+ val cardParams: Map,
+ )
+}
+
+internal class BlockchainSdkErrorConverter(
+ private val cardSdkErrorConverter: CardSdkErrorConverter,
+) : Converter {
+
+ override fun convert(value: Model): ErrorEvent? {
+ val error = (value.error as? BlockchainSdkError) ?: return null
+
+ if (error is BlockchainSdkError.WrappedTangemError) {
+ val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return null
+
+ val cardSdkModel = CardSdkErrorConverter.Model(
+ error = tangemSdkError,
+ action = value.action,
+ params = value.params,
+ cardParams = value.cardParams,
+ )
+ return cardSdkErrorConverter.convert(cardSdkModel)
+ }
+
+ if (error.customMessage.contains(DemoTransactionSender.ID)) return null
+
+ val mutableParams = value.params.convertToParamValue().toMutableMap()
+ mutableParams[AnalyticsParam.ACTION.param] = value.action.key
+ mutableParams[AnalyticsParam.ERROR_CODE.param] = error.code.toString()
+ mutableParams[AnalyticsParam.ERROR_DESCRIPTION.param] = "${error.javaClass.simpleName}: ${error.customMessage}"
+ mutableParams[AnalyticsParam.ERROR_KEY.param] = "BlockchainSdkError"
+
+ return ErrorEvent(value.error, mutableParams)
+ }
+
+ data class Model(
+ val error: BlockchainError,
+ val action: Analytics.ActionToLog,
+ val params: Map,
+ val cardParams: Map,
+ )
+}
+
+internal fun Map.convertToParamValue(): Map {
+ return mutableMapOf().also { newMap ->
+ this.forEach {
+ newMap[it.key.param] = it.value
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt
new file mode 100644
index 0000000000..ab530b5829
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt
@@ -0,0 +1,23 @@
+package com.tangem.tap.common.analytics.handlers.amplitude
+
+import com.tangem.common.card.Card
+import com.tangem.tap.common.analytics.AnalyticsEvent
+import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
+
+class AmplitudeAnalyticsHandler(
+ private val client: AmplitudeAnalyticsClient,
+) : AnalyticsEventHandler {
+
+ override fun handleEvent(event: String, params: Map) {
+ client.logEvent(event, params)
+ }
+
+ override fun handleAnalyticsEvent(
+ event: AnalyticsEvent,
+ params: Map,
+ card: Card?,
+ blockchain: String?,
+ ) {
+ handleEvent(event.event, prepareParams(card, blockchain, params))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt
new file mode 100644
index 0000000000..d90ed084b5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt
@@ -0,0 +1,36 @@
+package com.tangem.tap.common.analytics.handlers.amplitude
+
+import android.app.Application
+import com.amplitude.api.Amplitude
+import com.amplitude.api.AmplitudeClient
+import com.tangem.common.Converter
+import com.tangem.tap.common.analytics.api.EventLogger
+import org.json.JSONObject
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface AmplitudeAnalyticsClient : EventLogger
+
+internal class AmplitudeClient(
+ application: Application,
+ key: String,
+) : AmplitudeAnalyticsClient {
+
+ private val client: AmplitudeClient = Amplitude.getInstance()
+
+ init {
+ client.initialize(application, key)
+ client.enableForegroundTracking(application)
+ }
+
+ override fun logEvent(event: String, params: Map) {
+ client.logEvent(event, ParamsToJSONObjectConverter().convert(params))
+ }
+}
+
+private class ParamsToJSONObjectConverter : Converter