Updated on 2026-08-14

This commit is contained in:
Tangem 2022-09-23 02:11:50 +03:00
parent 53f9b9d572
commit d17d9e35bf
28 changed files with 514 additions and 455 deletions

View file

@ -122,9 +122,8 @@ 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'
// Analytics
implementation 'com.appsflyer:af-android-sdk:6.5.1'

View file

@ -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.AnalyticsEventHandlerBuilder
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
@ -95,15 +95,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.SetGlobalAnalyticsHandler(analyticsHandler))
private fun initAnalytics(application: Application, config: Config) {
val globalAnalyticsHandler = AnalyticsEventHandlerBuilder(
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) {

View file

@ -1,56 +0,0 @@
package com.tangem.tap.common.analytics
import com.amplitude.api.Amplitude
import com.amplitude.api.AmplitudeClient
import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import org.json.JSONObject
class AmplitudeAnalyticsHandler : AnalyticsHandler() {
private val client: AmplitudeClient = Amplitude.getInstance()
override fun triggerEvent(
event: AnalyticsEvent,
card: Card?,
blockchain: String?,
params: Map<String, String>
) {
client.logEvent(event.event, prepareParams(card, blockchain, params).toJson())
}
override fun triggerEvent(event: String, params: Map<String, String>) {
client.logEvent(event, params.toJson())
}
override fun logCardSdkError(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>?,
card: Card?
) {
//TODO
}
override fun logError(error: Throwable, params: Map<String, String>) {
//TODO
}
override fun getOrderEvent(): String {
//TODO
return ""
}
override fun getOrderParams(order: Storefront.Order): Map<String, String> {
//TODO
return emptyMap()
}
private fun Map<String, String>.toJson(): JSONObject {
val json = JSONObject()
this.map { json.put(it.key, it.value) }
return json
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.tap.common.analytics
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.extensions.filterNotNull
interface EventHandler {
fun handleEvent(
event: String,
params: Map<String, String> = emptyMap(),
)
}
interface AnalyticsEventHandler : EventHandler {
fun handleAnalyticsEvent(
event: AnalyticsEvent,
params: Map<String, String> = emptyMap(),
card: Card? = null,
blockchain: String? = null,
)
fun prepareParams(
card: Card?,
blockchain: String? = null,
params: Map<String, String> = emptyMap(),
): Map<String, String> {
return mapOf(
AnalyticsParam.FIRMWARE.param to card?.firmwareVersion?.stringValue,
AnalyticsParam.BATCH_ID.param to card?.batchId,
AnalyticsParam.BLOCKCHAIN.param to blockchain,
).filterNotNull() + params
}
}
interface ShopifyOrderEventHandler {
fun handleShopifyOrderEvent(order: Storefront.Order)
}
interface ErrorEventHandler {
fun handleErrorEvent(
error: Throwable,
params: Map<String, String> = emptyMap(),
)
}
interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler
interface CardSdkErrorEventHandler {
fun handleCardSdkErrorEvent(
error: TangemSdkError,
action: Analytics.ActionToLog,
params: Map<AnalyticsParam, String> = emptyMap(),
card: Card? = null,
)
}
interface BlockchainSdkErrorEventHandler {
fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: Analytics.ActionToLog,
params: Map<AnalyticsParam, String> = mapOf(),
card: Card? = null,
)
}

View file

@ -0,0 +1,66 @@
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.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.domain.configurable.config.Config
/**
[REDACTED_AUTHOR]
*/
class AnalyticsEventHandlerBuilder(
private val application: Application,
private val config: Config,
private val isDebug: Boolean,
private val logConfig: AnalyticsHandlersLogConfig,
private val jsonConverter: MoshiJsonConverter,
) {
private val analyticHandlers = mutableListOf<AnalyticsEventHandler>()
fun build(): GlobalAnalyticsEventHandler {
return GlobalAnalyticsHandler(analyticHandlers.toList())
}
fun default(): GlobalAnalyticsEventHandler {
addFirebaseAnalytics()
addAppsFlyer()
addAmplitude()
return GlobalAnalyticsHandler(analyticHandlers.toList())
}
fun addFirebaseAnalytics(): AnalyticsEventHandlerBuilder {
if (isDebug) {
addEventLogger(logConfig.firebase, FirebaseAnalyticsHandler::class.java.simpleName)
} else {
analyticHandlers.add(FirebaseAnalyticsHandler())
}
return this
}
fun addAppsFlyer(): AnalyticsEventHandlerBuilder {
if (isDebug) {
addEventLogger(logConfig.appsFlyer, AppsFlyerAnalyticsHandler::class.java.simpleName)
} else {
analyticHandlers.add(AppsFlyerAnalyticsHandler(application, config.appsFlyerDevKey))
}
return this
}
fun addAmplitude(): AnalyticsEventHandlerBuilder {
if (isDebug) {
addEventLogger(logConfig.amplitude, AmplitudeAnalyticsHandler::class.java.simpleName)
} else {
analyticHandlers.add(AmplitudeAnalyticsHandler(application, config.amplitudeApiKey))
}
return this
}
private fun addEventLogger(logsEnabled: Boolean, name: String) {
// if (logsEnabled) analyticHandlers.add(AnalyticsLogsPrinter(name, jsonConverter))
}
}

View file

@ -1,93 +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<String, String> = emptyMap()
)
abstract fun triggerEvent(
event: String,
params: Map<String, String>
)
abstract fun logCardSdkError(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>? = null,
card: Card? = null
)
abstract fun logError(
error: Throwable,
params: Map<String, String> = emptyMap()
)
protected fun prepareParams(
card: Card?,
blockchain: String? = null,
params: Map<String, String> = emptyMap()
): Map<String, String> {
return mapOf(
AnalyticsParam.FIRMWARE.param to card?.firmwareVersion?.stringValue,
AnalyticsParam.BATCH_ID.param to card?.batchId,
AnalyticsParam.BLOCKCHAIN.param to blockchain,
).filterNotNull() + params
}
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<String, String>
}

View file

@ -1,65 +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<String, String>
) {
AppsFlyerLib.getInstance().logEvent(
context,
event.event,
prepareParams(card, blockchain, params)
)
}
override fun logCardSdkError(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>?,
card: Card?
) {
//TODO
}
override fun logError(error: Throwable, params: Map<String, String>) {
//TODO
}
override fun getOrderEvent(): String = AFInAppEventType.PURCHASE
override fun getOrderParams(order: Storefront.Order): Map<String, String> {
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<String, String>) {
AppsFlyerLib.getInstance().logEvent(context, event, params)
}
}

View file

@ -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<String, String>
) {
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<String, String>.toBundle(): Bundle {
return bundleOf(*this.toList().toTypedArray())
}
override fun logCardSdkError(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>?,
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<String, String>) {
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<String, String> {
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<String, String>) {
Firebase.analytics.logEvent(event, params.toBundle())
}
}

View file

@ -1,77 +1,103 @@
package com.tangem.tap.common.analytics
import android.content.Context
import com.amplitude.api.Amplitude
import com.appsflyer.AppsFlyerLib
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.store
import com.tangem.tap.common.extensions.filterNotNull
class GlobalAnalyticsHandler(val analyticsHandlers: List<AnalyticsHandler>) :
AnalyticsHandler() {
override fun triggerEvent(
interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
ShopifyOrderEventHandler,
ErrorEventHandler,
SdkErrorEventHandler
class GlobalAnalyticsHandler(
private val analyticsHandlers: List<AnalyticsEventHandler>,
) : GlobalAnalyticsEventHandler {
override fun handleEvent(event: String, params: Map<String, String>) {
analyticsHandlers.forEach { it.handleEvent(event, params) }
}
override fun handleAnalyticsEvent(
event: AnalyticsEvent,
params: Map<String, String>,
card: Card?,
blockchain: String?,
params: Map<String, String>
) {
analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain, params) }
analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) }
}
override fun triggerEvent(event: String, params: Map<String, String>) {
analyticsHandlers.forEach { it.triggerEvent(event, params) }
override fun handleShopifyOrderEvent(order: Storefront.Order) {
analyticsHandlers.filterIsInstance<ShopifyOrderEventHandler>().forEach {
it.handleShopifyOrderEvent(order)
}
}
override fun logCardSdkError(
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {
analyticsHandlers.filterIsInstance<ErrorEventHandler>().forEach {
it.handleErrorEvent(error, params)
}
}
override fun handleCardSdkErrorEvent(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>?,
card: Card?
action: Analytics.ActionToLog,
params: Map<AnalyticsParam, String>,
card: Card?,
) {
analyticsHandlers.forEach { it.logCardSdkError(error, actionToLog, parameters, card) }
analyticsHandlers.filterIsInstance<CardSdkErrorEventHandler>().forEach {
it.handleCardSdkErrorEvent(error, action, params, card)
}
}
override fun logError(error: Throwable, params: Map<String, String>) {
analyticsHandlers.forEach { it.logError(error, params) }
override fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: Analytics.ActionToLog,
params: Map<AnalyticsParam, String>,
card: Card?,
) {
analyticsHandlers.filterIsInstance<BlockchainSdkErrorEventHandler>().forEach {
it.handleBlockchainSdkErrorEvent(error, action, params, card)
}
}
}
override fun getOrderEvent(): String {
return ""
}
override fun getOrderParams(order: Storefront.Order): Map<String, String> {
return emptyMap()
}
companion object {
fun createDefaultAnalyticHandlers(context: Context): GlobalAnalyticsHandler {
return GlobalAnalyticsHandler(
listOf(
FirebaseAnalyticsHandler,
AppsFlyerAnalyticsHandler(context),
AmplitudeAnalyticsHandler()
)
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,
),
)
}
private fun initAnalytics(context: Context) {
initAppsFlyer(context)
initAmplitude(context)
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)
}
private fun initAppsFlyer(context: Context) {
val devKey = store.state.globalState.configManager?.config?.appsFlyerDevKey ?: return
AppsFlyerLib.getInstance().init(devKey, null, context)
AppsFlyerLib.getInstance().start(context)
}
private fun initAmplitude(context: Context) {
val apiKey = store.state.globalState.configManager?.config?.amplitudeApiKey ?: return
Amplitude.getInstance()
.initialize(context.applicationContext, apiKey)
.enableForegroundTracking(context)
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(),
)
}
}
}

View file

@ -0,0 +1,43 @@
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.common.card.Card
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsEventHandler
import org.json.JSONObject
class AmplitudeAnalyticsHandler(
application: Application,
key: String,
) : AnalyticsEventHandler {
private val client: AmplitudeClient = Amplitude.getInstance()
init {
client.initialize(application, key)
client.enableForegroundTracking(application)
}
override fun handleEvent(event: String, params: Map<String, String>) {
client.logEvent(event, ParamsJSONObjectConverter().convert(params))
}
override fun handleAnalyticsEvent(
event: AnalyticsEvent,
params: Map<String, String>,
card: Card?,
blockchain: String?,
) {
handleEvent(event.event, prepareParams(card, blockchain, params))
}
class ParamsJSONObjectConverter : Converter<Map<String, String>, JSONObject> {
override fun convert(value: Map<String, String>): JSONObject = JSONObject().apply {
value.forEach { this.put(it.key, it.value) }
}
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
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.Converter
import com.tangem.common.card.Card
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsEventHandler
import com.tangem.tap.common.analytics.ShopifyOrderEventHandler
class AppsFlyerAnalyticsHandler(
private val context: Context,
key: String,
) : AnalyticsEventHandler, ShopifyOrderEventHandler {
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
init {
appsFlyerLib.init(key, null, context)
appsFlyerLib.start(context)
}
override fun handleEvent(event: String, params: Map<String, String>) {
appsFlyerLib.logEvent(context, event, params)
}
override fun handleAnalyticsEvent(
event: AnalyticsEvent,
params: Map<String, String>,
card: Card?,
blockchain: String?,
) {
handleEvent(event.event, prepareParams(card, blockchain, params))
}
override fun handleShopifyOrderEvent(order: Storefront.Order) {
handleEvent(ORDER_EVENT, OrderToParamsConverter().convert(order))
}
companion object {
const val ORDER_EVENT = AFInAppEventType.PURCHASE
}
class OrderToParamsConverter : Converter<Storefront.Order, Map<String, String>> {
override fun convert(value: Storefront.Order): Map<String, String> {
val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
val discountCode =
(value.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 value.totalPriceV2.amount,
AFInAppEventParameterName.CURRENCY to value.currencyCode.name,
) + discountParams
}
}
}

View file

@ -0,0 +1,127 @@
package com.tangem.tap.common.analytics.handlers.firebase
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.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.shopify.buy3.Storefront
import com.tangem.blockchain.common.BlockchainError
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.Converter
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.AnalyticsEventHandler
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.ErrorEventHandler
import com.tangem.tap.common.analytics.SdkErrorEventHandler
import com.tangem.tap.common.analytics.ShopifyOrderEventHandler
import com.tangem.tap.common.analytics.TangemSdk
import com.tangem.tap.features.demo.DemoTransactionSender
class FirebaseAnalyticsHandler : AnalyticsEventHandler, ShopifyOrderEventHandler, ErrorEventHandler,
SdkErrorEventHandler {
private val fbAnalytics = Firebase.analytics
private val fbCrashlytics = Firebase.crashlytics
override fun handleEvent(event: String, params: Map<String, String>) {
fbAnalytics.logEvent(event, params.toBundle())
}
override fun handleAnalyticsEvent(
event: AnalyticsEvent,
params: Map<String, String>,
card: Card?,
blockchain: String?,
) {
handleEvent(event.event, prepareParams(card, blockchain, params))
}
override fun handleShopifyOrderEvent(order: Storefront.Order) {
handleEvent(ORDER_EVENT, OrderToParamsConverter().convert(order))
}
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {
params.forEach { (key, value) -> fbCrashlytics.setCustomKey(key, value) }
fbCrashlytics.recordException(error)
}
override fun handleCardSdkErrorEvent(
error: TangemSdkError,
action: Analytics.ActionToLog,
params: Map<AnalyticsParam, String>,
card: Card?,
) {
if (error is TangemSdkError.UserCancelled) return
val mutableParams = params.toMutableMap()
if (card != null) mutableParams + prepareParams(card)
mutableParams[AnalyticsParam.ACTION] = action.key
mutableParams[AnalyticsParam.ERROR_CODE] = error.code.toString()
mutableParams[AnalyticsParam.ERROR_DESCRIPTION] = error.javaClass.simpleName
mutableParams[AnalyticsParam.ERROR_KEY] = "TangemSdkError"
mutableParams.forEach { fbCrashlytics.setCustomKey(it.key.param, it.value) }
fbCrashlytics.recordException(TangemSdk.map(error))
}
override fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: Analytics.ActionToLog,
params: Map<AnalyticsParam, String>,
card: Card?,
) {
val blockchainSdkError = (error as? BlockchainSdkError) ?: return
if (blockchainSdkError is BlockchainSdkError.WrappedTangemError) {
val tangemSdkError = (blockchainSdkError.tangemError as? TangemSdkError) ?: return
handleCardSdkErrorEvent(tangemSdkError, action, params, card)
return
}
if (blockchainSdkError.customMessage.contains(DemoTransactionSender.ID)) return
val mutableParams = params.toMutableMap()
mutableParams[AnalyticsParam.ACTION] = action.key
mutableParams[AnalyticsParam.ERROR_CODE] = error.code.toString()
mutableParams[AnalyticsParam.ERROR_DESCRIPTION] = "${error.javaClass.simpleName}: ${error.customMessage}"
mutableParams[AnalyticsParam.ERROR_KEY] = "BlockchainSdkError"
mutableParams.forEach { fbCrashlytics.setCustomKey(it.key.param, it.value) }
fbCrashlytics.recordException(error)
}
private fun Map<String, String>.toBundle(): Bundle {
return bundleOf(*this.toList().toTypedArray())
}
companion object {
const val ORDER_EVENT = FirebaseAnalytics.Event.PURCHASE
}
class OrderToParamsConverter : Converter<Storefront.Order, Map<String, String>> {
override fun convert(value: Storefront.Order): Map<String, String> {
val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
val discountCode =
(value.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 value.totalPriceV2.amount,
FirebaseAnalytics.Param.CURRENCY to value.currencyCode.name,
) + discountParams
}
}
}

View file

@ -1,51 +0,0 @@
package com.tangem.tap.common.extensions
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.common.BlockchainError
import com.tangem.blockchain.common.BlockchainSdkError
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.AnalyticsHandler
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.features.demo.DemoTransactionSender
/**
[REDACTED_AUTHOR]
*/
fun AnalyticsHandler.logSendTransactionError(
error: BlockchainError,
action: Analytics.ActionToLog,
parameters: Map<AnalyticsParam, String>? = mapOf(),
card: Card? = null,
) {
when (val blockchainSdkError = (error as BlockchainSdkError)) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = (blockchainSdkError.tangemError as? TangemSdkError) ?: return
logCardSdkError(
error = tangemSdkError,
actionToLog = action,
parameters = parameters,
card = card,
)
}
else -> {
when {
blockchainSdkError.customMessage.contains(DemoTransactionSender.ID) -> return
else -> {
val params = parameters?.toMutableMap() ?: mutableMapOf()
params[AnalyticsParam.ACTION] = action.key
params[AnalyticsParam.ERROR_CODE] = error.code.toString()
params[AnalyticsParam.ERROR_DESCRIPTION] = "${error.javaClass.simpleName}: ${error.customMessage}"
params[AnalyticsParam.ERROR_KEY] = "BlockchainSdkError"
FirebaseCrashlytics.getInstance().apply {
params.forEach { setCustomKey(it.key.param, it.value) }
recordException(error)
}
}
}
}
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
@ -75,7 +75,7 @@ sealed class GlobalAction : Action {
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsHandler) : GlobalAction()
data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsEventHandler) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData) : GlobalAction()

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
@ -27,7 +27,7 @@ data class GlobalState(
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val resources: AndroidResources = AndroidResources(),
val analyticsHandler: GlobalAnalyticsHandler? = null,
val analyticsHandler: GlobalAnalyticsEventHandler? = null,
val userCountryCode: String? = null,
) : StateType

View file

@ -5,7 +5,7 @@ import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
import com.tangem.common.extensions.ifNotNull
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
@ -196,10 +196,10 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
return checkouts[productType]!!.webUrl
}
suspend fun waitForCheckout(productType: ProductType, analyticsHandler: GlobalAnalyticsHandler?) {
suspend fun waitForCheckout(productType: ProductType, analyticsHandler: GlobalAnalyticsEventHandler?) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
result.onSuccess {
ifNotNull(analyticsHandler, it.order) { handler, order -> handler.logShopifyOrder(order) }
ifNotNull(analyticsHandler, it.order) { handler, order -> handler.handleShopifyOrderEvent(order) }
}
}

View file

@ -28,7 +28,7 @@ import com.tangem.operations.pins.SetUserCodeCommand
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.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
@ -44,12 +44,12 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
suspend fun scanProduct(
analyticsHandler: GlobalAnalyticsHandler?,
analyticsHandler: GlobalAnalyticsEventHandler?,
userTokensRepository: UserTokensRepository,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler?.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEvent.READY_TO_SCAN, card = null)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
@ -69,30 +69,30 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private fun sendScanResultsToAnalytics(
analyticsHandler: GlobalAnalyticsHandler?,
analyticsHandler: GlobalAnalyticsEventHandler?,
result: CompletionResult<ScanResponse>
) {
when (result) {
is CompletionResult.Success -> {
analyticsHandler?.triggerEvent(
analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEvent.CARD_IS_SCANNED,
card = result.data.card,
blockchain = result.data.walletData?.blockchain
)
if (DemoHelper.isDemoCard(result.data)) {
analyticsHandler?.triggerEvent(
analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEvent.DEMO_MODE_ACTIVATED,
params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId),
card = result.data.card,
blockchain = result.data.walletData?.blockchain,
params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId)
blockchain = result.data.walletData?.blockchain
)
}
}
is CompletionResult.Failure ->
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler?.logCardSdkError(
analyticsHandler?.handleCardSdkErrorEvent(
error = error,
actionToLog = Analytics.ActionToLog.Scan
action = Analytics.ActionToLog.Scan
)
}
}

View file

@ -15,13 +15,13 @@ import com.tangem.network.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
private val card: Card,
assetReader: AssetReader,
val analyticsHandler: GlobalAnalyticsHandler?
val analyticsHandler: GlobalAnalyticsEventHandler?
) {
private val currentCardId: String = card.cardId
@ -37,7 +37,7 @@ class TwinCardsManager(
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
analyticsHandler?.logCardSdkError(
analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.CreateWallet,
card = card
@ -67,7 +67,7 @@ class TwinCardsManager(
}
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
analyticsHandler?.logCardSdkError(
analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.CreateWallet,
card = card
@ -87,7 +87,7 @@ class TwinCardsManager(
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
analyticsHandler?.logCardSdkError(
analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.WriteIssuerData,
card = card

View file

@ -5,6 +5,7 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.logWcEvent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction

View file

@ -25,7 +25,6 @@ import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.logSendTransactionError
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
@ -144,7 +143,7 @@ class WalletConnectSdkHelper {
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
}
is SimpleResult.Failure -> {
store.state.globalState.analyticsHandler?.logSendTransactionError(
store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent(
result.error,
Analytics.ActionToLog.WalletConnectTransaction,
)
@ -174,7 +173,7 @@ class WalletConnectSdkHelper {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.WalletConnectSign,
)
@ -203,7 +202,7 @@ class WalletConnectSdkHelper {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.WalletConnectTransaction,
)
@ -279,7 +278,7 @@ class WalletConnectSdkHelper {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.WalletConnectSign,
)

View file

@ -115,7 +115,7 @@ class DetailsMiddleware {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.PurgeWallet,
card = store.state.detailsState.scanResponse?.card,
@ -159,10 +159,10 @@ class DetailsMiddleware {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error = error,
actionToLog = Analytics.ActionToLog.ChangeSecOptions,
parameters = mapOf(
action = Analytics.ActionToLog.ChangeSecOptions,
params = mapOf(
AnalyticsParam.NEW_SECURITY_OPTION to
(selectedOption?.name ?: ""),
),

View file

@ -62,7 +62,7 @@ private val homeMiddleware: Middleware<AppState> = { _, _ ->
store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.state.globalState.analyticsHandler?.triggerEvent(
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param)
)

View file

@ -23,7 +23,6 @@ import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.logSendTransactionError
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppDialog
@ -222,7 +221,7 @@ private fun sendTransaction(
when (sendResult) {
is SimpleResult.Success -> {
store.state.globalState.analyticsHandler?.triggerEvent(
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEvent.TRANSACTION_IS_SENT,
card = card,
blockchain = walletManager.wallet.blockchain.currency,
@ -252,10 +251,10 @@ private fun sendTransaction(
feeAmount = feeAmount,
destinationAddress = destinationAddress,
)
store.state.globalState.analyticsHandler?.logSendTransactionError(
store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent(
error = sendResult.error,
action = Analytics.ActionToLog.SendTransaction,
parameters = mapOf(AnalyticsParam.BLOCKCHAIN to walletManager.wallet.blockchain.currency),
params = mapOf(AnalyticsParam.BLOCKCHAIN to walletManager.wallet.blockchain.currency),
card = card,
)

View file

@ -174,7 +174,7 @@ class WalletMiddleware {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.CreateWallet,
card = store.state.detailsState.scanResponse?.card

View file

@ -81,7 +81,7 @@ class WarningsMiddleware {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
store.state.globalState.analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED)
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(AnalyticsEvent.APP_RATING_DISPLAYED)
addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
}
}

View file

@ -109,12 +109,12 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.btnClose.show()
val analyticsHandler = store.state.globalState.analyticsHandler
binding.btnClose.setOnClickListener {
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEvent.APP_RATING_DISMISS)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
binding.btnCanBeBetter.setOnClickListener {
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
@ -122,7 +122,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.btnReallyCool.setOnClickListener {
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEvent.APP_RATING_POSITIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()

View file

@ -0,0 +1,8 @@
package com.tangem.common
/**
[REDACTED_AUTHOR]
*/
interface Converter<I, O> {
fun convert(value: I): O
}

View file

@ -7,6 +7,7 @@ object LogConfig {
val storeAction: Boolean = BuildConfig.DEBUG
val zendesk: Boolean = false
val network: NetworkLogConfig = NetworkLogConfig
val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig
}
object NetworkLogConfig {
@ -14,4 +15,10 @@ object NetworkLogConfig {
val moonPayService: Boolean = false
val tangemTechService: Boolean = BuildConfig.DEBUG
val blockchainSdkNetwork: Boolean = BuildConfig.DEBUG
}
object AnalyticsHandlersLogConfig {
val firebase: Boolean = false
val appsFlyer: Boolean = false
val amplitude: Boolean = false
}