Skip to content

Commit

Permalink
Add Link attestation analytics (#10194)
Browse files Browse the repository at this point in the history
  • Loading branch information
toluo-stripe authored Feb 18, 2025
1 parent 48ba611 commit 46e6121
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.stripe.android.testing

import androidx.annotation.RestrictTo
import app.cash.turbine.Turbine
import com.stripe.android.core.exception.StripeException
import com.stripe.android.payments.core.analytics.ErrorReporter

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class FakeErrorReporter : ErrorReporter {

private val calls = Turbine<Call>()
private val loggedErrors: MutableList<String> = mutableListOf()

override fun report(
Expand All @@ -15,6 +16,13 @@ class FakeErrorReporter : ErrorReporter {
additionalNonPiiParams: Map<String, String>,
) {
loggedErrors.add(errorEvent.eventName)
calls.add(
item = Call(
errorEvent = errorEvent,
stripeException = stripeException,
additionalNonPiiParams = additionalNonPiiParams
)
)
}

fun getLoggedErrors(): List<String> {
Expand All @@ -24,4 +32,18 @@ class FakeErrorReporter : ErrorReporter {
fun clear() {
loggedErrors.clear()
}

suspend fun awaitCall(): Call {
return calls.awaitItem()
}

fun ensureAllEventsConsumed() {
calls.ensureAllEventsConsumed()
}

data class Call(
val errorEvent: ErrorReporter.ErrorEvent,
val stripeException: StripeException?,
val additionalNonPiiParams: Map<String, String>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ interface ErrorReporter : FraudDetectionErrorReporter {
LINK_LOG_OUT_FAILURE(
eventName = "link.log_out.failure"
),
LINK_NATIVE_FAILED_TO_GET_INTEGRITY_TOKEN(
eventName = "link.native.failed_to_get_integrity_token"
),
LINK_NATIVE_FAILED_TO_ATTEST_REQUEST(
eventName = "link.native.failed_to_attest_request"
),
LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER(
eventName = "link.native.integrity.preparation_failed"
),
Expand Down Expand Up @@ -198,9 +204,6 @@ interface ErrorReporter : FraudDetectionErrorReporter {
LINK_WEB_FAILED_TO_PARSE_RESULT_URI(
partialEventName = "link.web.result.parsing_failed"
),
LINK_NATIVE_FAILED_TO_ATTEST_REQUEST(
partialEventName = "link.native.failed_to_attest_request"
),
LINK_NATIVE_FAILED_TO_ATTEST_SIGNUP_REQUEST(
partialEventName = "link.native.signup.failed_to_attest_request"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.stripe.android.link.account

import com.stripe.android.common.di.APPLICATION_ID
import com.stripe.android.core.exception.APIException
import com.stripe.android.link.LinkEventException
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.inline.SignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.attestation.AttestationError
import com.stripe.attestation.IntegrityRequestManager
import javax.inject.Inject
Expand All @@ -15,6 +17,7 @@ internal class DefaultLinkAuth @Inject constructor(
private val linkGate: LinkGate,
private val linkAccountManager: LinkAccountManager,
private val integrityRequestManager: IntegrityRequestManager,
private val errorReporter: ErrorReporter,
@Named(APPLICATION_ID) private val applicationId: String
) : LinkAuth {
override suspend fun signUp(
Expand Down Expand Up @@ -82,6 +85,8 @@ internal class DefaultLinkAuth @Inject constructor(
verificationToken = verificationToken,
appId = applicationId
).getOrThrow()
}.onFailure { error ->
reportError(error, operation = "signup")
}
}

Expand All @@ -99,9 +104,30 @@ internal class DefaultLinkAuth @Inject constructor(
appId = applicationId,
startSession = startSession
).getOrThrow()
}.onFailure { error ->
reportError(error, operation = "lookup")
}
}

private fun reportError(error: Throwable, operation: String) {
val errorEvent = when {
error.isBackendAttestationError -> {
ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_ATTEST_REQUEST
}
error.isIntegrityManagerError -> {
ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_GET_INTEGRITY_TOKEN
}
else -> return
}
errorReporter.report(
errorEvent = errorEvent,
stripeException = LinkEventException(error),
additionalNonPiiParams = mapOf(
"operation" to operation
)
)
}

private fun Result<LinkAccount?>.toLinkAuthResult(): LinkAuthResult {
return runCatching {
val linkAccount = getOrThrow()
Expand Down Expand Up @@ -130,13 +156,15 @@ internal class DefaultLinkAuth @Inject constructor(
}

private val Throwable.isAttestationError: Boolean
get() = when (this) {
// Stripe backend could not verify the intregrity of the request
is APIException -> stripeError?.code == "link_failed_to_attest_request"
// Interaction with Integrity API to generate tokens resulted in a failure
is AttestationError -> true
else -> false
}
get() = isIntegrityManagerError || isBackendAttestationError

// Interaction with Integrity API to generate tokens resulted in a failure
private val Throwable.isIntegrityManagerError: Boolean
get() = this is AttestationError

// Stripe backend could not verify the integrity of the request
private val Throwable.isBackendAttestationError: Boolean
get() = this is APIException && stripeError?.code == "link_failed_to_attest_request"

private val Throwable.isAccountError: Boolean
get() = when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,19 @@ import com.stripe.android.link.FakeIntegrityRequestManager
import com.stripe.android.link.TestFactory
import com.stripe.android.link.gate.FakeLinkGate
import com.stripe.android.link.ui.inline.SignUpConsentAction
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.testing.CoroutineTestRule
import com.stripe.android.testing.FakeErrorReporter
import com.stripe.attestation.AttestationError
import com.stripe.attestation.IntegrityRequestManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test

internal class DefaultLinkAuthTest {

private val dispatcher = UnconfinedTestDispatcher()

@Before
fun before() {
Dispatchers.setMain(dispatcher)
}

@After
fun cleanup() {
Dispatchers.resetMain()
}
@get:Rule
val testRule = CoroutineTestRule()

@Test
fun `config with attestation enabled successfully signs in`() = runTest {
Expand Down Expand Up @@ -68,20 +57,22 @@ internal class DefaultLinkAuthTest {
}

@Test
fun `sign in attempt with attestation failure returns AttestationFailed`() = runTest {
fun `sign up attempt with attestation failure returns AttestationFailed`() = runTest {
val error = APIException(
stripeError = StripeError(
code = "link_failed_to_attest_request"
)
)
val errorReporter = FakeErrorReporter()
val linkAccountManager = FakeLinkAccountManager()
val integrityRequestManager = FakeIntegrityRequestManager()

integrityRequestManager.requestResult = Result.failure(error)

val linkAuth = linkAuth(
linkAccountManager = linkAccountManager,
integrityRequestManager = integrityRequestManager
integrityRequestManager = integrityRequestManager,
errorReporter = errorReporter
)

val result = linkAuth.signUp(
Expand All @@ -94,26 +85,35 @@ internal class DefaultLinkAuthTest {

integrityRequestManager.awaitRequestTokenCall()

val errorReport = errorReporter.awaitCall()
assertThat(errorReport.errorEvent)
.isEqualTo(ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_ATTEST_REQUEST)
assertThat(errorReport.additionalNonPiiParams)
.containsExactly("operation", "signup")

assertThat(result).isEqualTo(LinkAuthResult.AttestationFailed(error))

linkAccountManager.ensureAllEventsConsumed()
integrityRequestManager.ensureAllEventsConsumed()
errorReporter.ensureAllEventsConsumed()
}

@Test
fun `sign in attempt with token fetch failure returns AttestationFailed`() = runTest {
fun `sign up attempt with token fetch failure returns AttestationFailed`() = runTest {
val error = AttestationError(
errorType = AttestationError.ErrorType.INTERNAL_ERROR,
message = "oops"
)
val errorReporter = FakeErrorReporter()
val linkAccountManager = FakeLinkAccountManager()
val integrityRequestManager = FakeIntegrityRequestManager()

integrityRequestManager.requestResult = Result.failure(error)

val linkAuth = linkAuth(
linkAccountManager = linkAccountManager,
integrityRequestManager = integrityRequestManager
integrityRequestManager = integrityRequestManager,
errorReporter = errorReporter
)

val result = linkAuth.signUp(
Expand All @@ -126,10 +126,16 @@ internal class DefaultLinkAuthTest {

integrityRequestManager.awaitRequestTokenCall()

val errorReport = errorReporter.awaitCall()
assertThat(errorReport.errorEvent)
.isEqualTo(ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_GET_INTEGRITY_TOKEN)
assertThat(errorReport.additionalNonPiiParams)
.containsExactly("operation", "signup")
assertThat(result).isEqualTo(LinkAuthResult.AttestationFailed(error))

linkAccountManager.ensureAllEventsConsumed()
integrityRequestManager.ensureAllEventsConsumed()
errorReporter.ensureAllEventsConsumed()
}

@Test
Expand Down Expand Up @@ -270,12 +276,14 @@ internal class DefaultLinkAuthTest {
)
val linkAccountManager = FakeLinkAccountManager()
val integrityRequestManager = FakeIntegrityRequestManager()
val errorReporter = FakeErrorReporter()

integrityRequestManager.requestResult = Result.failure(error)

val linkAuth = linkAuth(
linkAccountManager = linkAccountManager,
integrityRequestManager = integrityRequestManager
integrityRequestManager = integrityRequestManager,
errorReporter = errorReporter
)

val result = linkAuth.lookUp(
Expand All @@ -286,10 +294,17 @@ internal class DefaultLinkAuthTest {

integrityRequestManager.awaitRequestTokenCall()

val errorReport = errorReporter.awaitCall()
assertThat(errorReport.errorEvent)
.isEqualTo(ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_ATTEST_REQUEST)
assertThat(errorReport.additionalNonPiiParams)
.containsExactly("operation", "lookup")

assertThat(result).isEqualTo(LinkAuthResult.AttestationFailed(error))

linkAccountManager.ensureAllEventsConsumed()
integrityRequestManager.ensureAllEventsConsumed()
errorReporter.ensureAllEventsConsumed()
}

@Test
Expand All @@ -300,12 +315,14 @@ internal class DefaultLinkAuthTest {
)
val linkAccountManager = FakeLinkAccountManager()
val integrityRequestManager = FakeIntegrityRequestManager()
val errorReporter = FakeErrorReporter()

integrityRequestManager.requestResult = Result.failure(error)

val linkAuth = linkAuth(
linkAccountManager = linkAccountManager,
integrityRequestManager = integrityRequestManager
integrityRequestManager = integrityRequestManager,
errorReporter = errorReporter
)

val result = linkAuth.lookUp(
Expand All @@ -316,10 +333,16 @@ internal class DefaultLinkAuthTest {

integrityRequestManager.awaitRequestTokenCall()

val errorReport = errorReporter.awaitCall()
assertThat(errorReport.errorEvent)
.isEqualTo(ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_GET_INTEGRITY_TOKEN)
assertThat(errorReport.additionalNonPiiParams)
.containsExactly("operation", "lookup")
assertThat(result).isEqualTo(LinkAuthResult.AttestationFailed(error))

linkAccountManager.ensureAllEventsConsumed()
integrityRequestManager.ensureAllEventsConsumed()
errorReporter.ensureAllEventsConsumed()
}

@Test
Expand Down Expand Up @@ -472,14 +495,16 @@ internal class DefaultLinkAuthTest {
private fun linkAuth(
useAttestationEndpoints: Boolean = true,
linkAccountManager: FakeLinkAccountManager = FakeLinkAccountManager(),
integrityRequestManager: IntegrityRequestManager = FakeIntegrityRequestManager()
integrityRequestManager: IntegrityRequestManager = FakeIntegrityRequestManager(),
errorReporter: ErrorReporter = FakeErrorReporter()
): DefaultLinkAuth {
return DefaultLinkAuth(
linkGate = FakeLinkGate().apply {
setUseAttestationEndpoints(useAttestationEndpoints)
},
linkAccountManager = linkAccountManager,
integrityRequestManager = integrityRequestManager,
errorReporter = errorReporter,
applicationId = TestFactory.APP_ID
)
}
Expand Down

0 comments on commit 46e6121

Please sign in to comment.