Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analytics - Track 3ds2 error events #1923

Merged
merged 10 commits into from
Dec 11, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ package com.adyen.checkout.adyen3ds2.internal.analytics
import androidx.annotation.RestrictTo
import com.adyen.checkout.components.core.internal.analytics.AnalyticsEvent
import com.adyen.checkout.components.core.internal.analytics.DirectAnalyticsEventCreation
import com.adyen.checkout.components.core.internal.analytics.ErrorEvent
import com.adyen.checkout.components.core.internal.analytics.GenericEvents

@OptIn(DirectAnalyticsEventCreation::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand Down Expand Up @@ -40,6 +42,24 @@ internal object ThreeDS2Events {
message = message,
)

fun threeDS2FingerprintError(
event: ErrorEvent,
message: String? = null
) = GenericEvents.error(
component = "threeDS2Fingerprint",
event = event,
message = message,
)

fun threeDS2ChallengeError(
event: ErrorEvent,
message: String? = null
) = GenericEvents.error(
component = "threeDS2Challenge",
event = event,
message = message,
)

enum class SubType(val value: String) {
FINGERPRINT_DATA_SENT("fingerprintDataSentMobile"),
FINGERPRINT_COMPLETED("fingerprintCompleted"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.adyen.checkout.components.core.internal.PaymentDataRepository
import com.adyen.checkout.components.core.internal.SavedStateHandleContainer
import com.adyen.checkout.components.core.internal.SavedStateHandleProperty
import com.adyen.checkout.components.core.internal.analytics.AnalyticsManager
import com.adyen.checkout.components.core.internal.analytics.ErrorEvent
import com.adyen.checkout.components.core.internal.analytics.GenericEvents
import com.adyen.checkout.components.core.internal.util.bufferedChannel
import com.adyen.checkout.core.AdyenLogLevel
Expand Down Expand Up @@ -147,6 +148,10 @@ internal class DefaultAdyen3DS2Delegate(
activity: Activity,
) {
if (action.token.isNullOrEmpty()) {
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TOKEN_MISSING,
message = "Token is missing for Threeds2FingerprintAction",
)
emitError(ComponentException("Fingerprint token not found."))
return
}
Expand All @@ -165,6 +170,10 @@ internal class DefaultAdyen3DS2Delegate(
activity: Activity,
) {
if (action.token.isNullOrEmpty()) {
trackChallengeErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TOKEN_MISSING,
message = "Token is missing for Threeds2ChallengeAction",
)
emitError(ComponentException("Challenge token not found."))
return
}
Expand All @@ -178,10 +187,6 @@ internal class DefaultAdyen3DS2Delegate(
action: Threeds2Action,
activity: Activity,
) {
if (action.token.isNullOrEmpty()) {
emitError(ComponentException("3DS2 token not found."))
return
}
if (action.subtype == null) {
emitError(ComponentException("3DS2 Action subtype not found."))
return
Expand All @@ -195,7 +200,25 @@ internal class DefaultAdyen3DS2Delegate(
activity: Activity,
subtype: Threeds2Action.SubType,
) {
val token = action.token.orEmpty()
val token = action.token
if (token.isNullOrEmpty()) {
val errorMessage = "Token is missing for Threeds2Action"
when (subtype) {
Threeds2Action.SubType.FINGERPRINT -> trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TOKEN_MISSING,
message = errorMessage,
)

Threeds2Action.SubType.CHALLENGE -> trackChallengeErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TOKEN_MISSING,
message = errorMessage,
)
}

emitError(ComponentException("3DS2 token not found."))
return
}

when (subtype) {
Threeds2Action.SubType.FINGERPRINT -> {
trackFingerprintActionEvent(action)
Expand All @@ -215,6 +238,7 @@ internal class DefaultAdyen3DS2Delegate(
}
}

@Suppress("LongMethod")
@VisibleForTesting
internal fun identifyShopper(
activity: Activity,
Expand All @@ -228,17 +252,26 @@ internal class DefaultAdyen3DS2Delegate(
val fingerprintToken = try {
decodeFingerprintToken(encodedFingerprintToken)
} catch (e: CheckoutException) {
trackFingerprintErrorEvent(ErrorEvent.THREEDS2_TOKEN_DECODING)
emitError(ComponentException("Failed to decode fingerprint token", e))
return
}

val configParameters = createAdyenConfigParameters(fingerprintToken) ?: run {
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_FINGERPRINT_CREATION,
message = "Fingerprint creation failed because the token is partial",
)
emitError(ComponentException("Failed to create ConfigParameters."))
return
}

val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
adyenLog(AdyenLogLevel.ERROR, throwable) { "Unexpected uncaught 3DS2 Exception" }
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_FINGERPRINT_CREATION,
message = "Fingerprint creation failed because of uncaught exception",
)
emitError(CheckoutException("Unexpected 3DS2 exception.", throwable))
}

Expand All @@ -248,18 +281,31 @@ internal class DefaultAdyen3DS2Delegate(

adyenLog(AdyenLogLevel.DEBUG) { "initialize 3DS2 SDK" }
val initializeResult =
threeDS2Service.initialize(activity, configParameters, null, componentParams.uiCustomization)
threeDS2Service.initialize(
activity,
configParameters,
null,
componentParams.uiCustomization,
)

if (initializeResult is InitializeResult.Failure) {
val details = makeDetails(initializeResult.transactionStatus, initializeResult.additionalDetails)
val details = makeDetails(
initializeResult.transactionStatus,
initializeResult.additionalDetails,
)
emitDetails(details)
return@launch
}

currentTransaction = createTransaction(fingerprintToken) ?: return@launch

val authenticationRequestParameters = currentTransaction?.authenticationRequestParameters
val authenticationRequestParameters =
currentTransaction?.authenticationRequestParameters
if (authenticationRequestParameters == null) {
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_FINGERPRINT_CREATION,
message = "Fingerprint creation failed because authentication parameters do not exist",
)
emitError(ComponentException("Failed to retrieve 3DS2 authentication parameters"))
return@launch
}
Expand Down Expand Up @@ -311,8 +357,14 @@ internal class DefaultAdyen3DS2Delegate(

private fun createTransaction(fingerprintToken: FingerprintToken): Transaction? {
if (fingerprintToken.threeDSMessageVersion == null) {
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TRANSACTION_CREATION,
message = "Transaction creation failed because threeDSMessageVersion is missing",
)

val error = "Failed to create 3DS2 Transaction. Missing threeDSMessageVersion inside fingerprintToken."
emitError(ComponentException(error))

return null
}

Expand All @@ -323,7 +375,9 @@ internal class DefaultAdyen3DS2Delegate(

return try {
adyenLog(AdyenLogLevel.DEBUG) { "create transaction" }
when (val result = threeDS2Service.createTransaction(null, fingerprintToken.threeDSMessageVersion)) {
val result =
threeDS2Service.createTransaction(null, fingerprintToken.threeDSMessageVersion)
when (result) {
is TransactionResult.Failure -> {
val details = makeDetails(result.transactionStatus, result.additionalDetails)
emitDetails(details)
Expand All @@ -333,9 +387,17 @@ internal class DefaultAdyen3DS2Delegate(
is TransactionResult.Success -> result.transaction
}
} catch (e: SDKNotInitializedException) {
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TRANSACTION_CREATION,
message = "Transaction creation failed because the SDK is not initialized",
)
emitError(ComponentException("Failed to create 3DS2 Transaction", e))
null
} catch (e: SDKRuntimeException) {
trackFingerprintErrorEvent(
errorEvent = ErrorEvent.THREEDS2_TRANSACTION_CREATION,
message = "Transaction creation failed because SDK threw runtime exception",
)
emitError(ComponentException("Failed to create 3DS2 Transaction", e))
null
}
Expand Down Expand Up @@ -373,7 +435,10 @@ internal class DefaultAdyen3DS2Delegate(
)
.fold(
onSuccess = { result -> onSubmitFingerprintResult(result, activity) },
onFailure = { e -> emitError(ComponentException("Unable to submit fingerprint", e)) },
onFailure = { e ->
trackFingerprintErrorEvent(ErrorEvent.THREEDS2_FINGERPRINT_HANDLING)
emitError(ComponentException("Unable to submit fingerprint", e))
},
)
}

Expand Down Expand Up @@ -425,6 +490,7 @@ internal class DefaultAdyen3DS2Delegate(
adyenLog(AdyenLogLevel.DEBUG) { "challengeShopper" }

if (currentTransaction == null) {
trackChallengeErrorEvent(ErrorEvent.THREEDS2_TRANSACTION_MISSING)
emitError(
Authentication3DS2Exception("Failed to make challenge, missing reference to initial transaction."),
)
Expand All @@ -435,7 +501,8 @@ internal class DefaultAdyen3DS2Delegate(
val challengeTokenJson: JSONObject = try {
JSONObject(decodedChallengeToken)
} catch (e: JSONException) {
emitError(ComponentException("JSON parsing of FingerprintToken failed", e))
trackChallengeErrorEvent(ErrorEvent.THREEDS2_TOKEN_DECODING)
emitError(ComponentException("JSON parsing of challenge token failed", e))
return
}

Expand All @@ -459,6 +526,10 @@ internal class DefaultAdyen3DS2Delegate(
)
analyticsManager?.trackEvent(challengeDisplayedEvent)
} catch (e: InvalidInputException) {
trackChallengeErrorEvent(
errorEvent = ErrorEvent.THREEDS2_CHALLENGE_HANDLING,
message = "Challenge failed because input is invalid",
)
emitError(CheckoutException("Error starting challenge", e))
}
}
Expand Down Expand Up @@ -492,6 +563,10 @@ internal class DefaultAdyen3DS2Delegate(
val details = makeDetails(transactionStatus)
emitDetails(details)
} catch (e: CheckoutException) {
trackChallengeErrorEvent(
errorEvent = ErrorEvent.THREEDS2_CHALLENGE_HANDLING,
message = "Challenge completed and details cannot be created",
)
emitError(e)
} finally {
closeTransaction()
Expand All @@ -510,18 +585,26 @@ internal class DefaultAdyen3DS2Delegate(
val details = makeDetails(result.transactionStatus, result.additionalDetails)
emitDetails(details)
} catch (e: CheckoutException) {
trackChallengeErrorEvent(
errorEvent = ErrorEvent.THREEDS2_CHALLENGE_HANDLING,
message = "Challenge timed out and details cannot be created",
)
emitError(e)
} finally {
closeTransaction()
}
}

private fun onError(result: ChallengeResult.Error) {
adyenLog(AdyenLogLevel.DEBUG) { "challenge timed out" }
adyenLog(AdyenLogLevel.DEBUG) { "challenge error" }
try {
val details = makeDetails(result.transactionStatus, result.additionalDetails)
emitDetails(details)
} catch (e: CheckoutException) {
trackChallengeErrorEvent(
errorEvent = ErrorEvent.THREEDS2_CHALLENGE_HANDLING,
message = "Challenge failed and details cannot be created",
)
emitError(e)
} finally {
closeTransaction()
Expand Down Expand Up @@ -560,9 +643,11 @@ internal class DefaultAdyen3DS2Delegate(
analyticsManager?.trackEvent(event)
}

private fun trackFingerprintActionEvent(action: Action) = trackActionEvent(action, ANALYTICS_MESSAGE_FINGERPRINT)
private fun trackFingerprintActionEvent(action: Action) =
trackActionEvent(action, "Fingerprint action was handled by the SDK")

private fun trackChallengeActionEvent(action: Action) = trackActionEvent(action, ANALYTICS_MESSAGE_CHALLENGE)
private fun trackChallengeActionEvent(action: Action) =
trackActionEvent(action, "Challenge action was handled by the SDK")

private fun trackActionEvent(action: Action, message: String) {
val event = GenericEvents.action(
Expand All @@ -573,6 +658,16 @@ internal class DefaultAdyen3DS2Delegate(
analyticsManager?.trackEvent(event)
}

private fun trackFingerprintErrorEvent(errorEvent: ErrorEvent, message: String? = null) {
val event = ThreeDS2Events.threeDS2FingerprintError(errorEvent, message)
analyticsManager?.trackEvent(event)
}

private fun trackChallengeErrorEvent(errorEvent: ErrorEvent, message: String? = null) {
val event = ThreeDS2Events.threeDS2ChallengeError(errorEvent, message)
analyticsManager?.trackEvent(event)
}

private fun closeTransaction() {
currentTransaction?.close()
currentTransaction = null
Expand Down Expand Up @@ -639,12 +734,6 @@ internal class DefaultAdyen3DS2Delegate(
}

companion object {
@VisibleForTesting
internal const val ANALYTICS_MESSAGE_FINGERPRINT = "Fingerprint action was handled by the SDK"

@VisibleForTesting
internal const val ANALYTICS_MESSAGE_CHALLENGE = "Challenge action was handled by the SDK"

private const val DEFAULT_CHALLENGE_TIME_OUT = 10
private const val PROTOCOL_VERSION_2_1_0 = "2.1.0"

Expand Down
Loading
Loading