From e87bbe9b8b880191abc4962bdc591ddb6e15f458 Mon Sep 17 00:00:00 2001 From: Lukasz Macionczyk Date: Thu, 23 Jan 2025 13:39:23 +0100 Subject: [PATCH] Auth V2 pixels (#5518) Task/Issue URL: https://app.asana.com/0/1205648422731273/1209217826294372/f ### Description This PRs adds a few pixels that will be useful for monitoring the rollout of auth API v2. ### Steps to test this PR QA-optional ### No UI changes --- .../impl/SubscriptionsManager.kt | 36 +++++++++++++------ .../impl/pixels/SubscriptionPixel.kt | 24 +++++++++++++ .../impl/pixels/SubscriptionPixelSender.kt | 36 +++++++++++++++++++ .../impl/RealSubscriptionsManagerTest.kt | 5 +++ 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt index e3570bad1edb..a5ff60ad5a6c 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt @@ -77,6 +77,7 @@ import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.Moshi import dagger.Lazy import dagger.SingleInstanceIn +import java.io.IOException import java.time.Duration import java.time.Instant import javax.inject.Inject @@ -537,14 +538,19 @@ class RealSubscriptionsManager @Inject constructor( } catch (e: HttpException) { if (e.code() == 401) { // refresh token is invalid / expired -> try to get a new pair of tokens using store login + pixelSender.reportAuthV2InvalidRefreshTokenDetected() val account = checkNotNull(authRepository.getAccount()) { "Missing account info when refreshing access token" } when (val storeLoginResult = storeLogin(account.externalId)) { - is StoreLoginResult.Success -> storeLoginResult.tokens + is StoreLoginResult.Success -> { + pixelSender.reportAuthV2InvalidRefreshTokenRecovered() + storeLoginResult.tokens + } StoreLoginResult.Failure.AccountExternalIdMismatch, StoreLoginResult.Failure.PurchaseHistoryNotAvailable, StoreLoginResult.Failure.AuthenticationError, -> { + pixelSender.reportAuthV2InvalidRefreshTokenSignedOut() signOut() throw e } @@ -873,15 +879,25 @@ class RealSubscriptionsManager @Inject constructor( } private suspend fun migrateToAuthV2() { - val accessTokenV1 = checkNotNull(authRepository.getAccessToken()) - val codeVerifier = pkceGenerator.generateCodeVerifier() - val codeChallenge = pkceGenerator.generateCodeChallenge(codeVerifier) - val sessionId = authClient.authorize(codeChallenge) - val authorizationCode = authClient.exchangeV1AccessToken(accessTokenV1, sessionId) - val tokens = authClient.getTokens(sessionId, authorizationCode, codeVerifier) - saveTokens(validateTokens(tokens)) - authRepository.setAccessToken(null) - authRepository.setAuthToken(null) + try { + val accessTokenV1 = checkNotNull(authRepository.getAccessToken()) + val codeVerifier = pkceGenerator.generateCodeVerifier() + val codeChallenge = pkceGenerator.generateCodeChallenge(codeVerifier) + val sessionId = authClient.authorize(codeChallenge) + val authorizationCode = authClient.exchangeV1AccessToken(accessTokenV1, sessionId) + val tokens = authClient.getTokens(sessionId, authorizationCode, codeVerifier) + saveTokens(validateTokens(tokens)) + authRepository.setAccessToken(null) + authRepository.setAuthToken(null) + pixelSender.reportAuthV2MigrationSuccess() + } catch (e: Exception) { + if (e is IOException) { + pixelSender.reportAuthV2MigrationFailureIo() + } else { + pixelSender.reportAuthV2MigrationFailureOther() + } + throw e + } } private fun isAccessTokenUsable(accessToken: AccessToken): Boolean { diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt index 3fc851e8450d..3d20075a7b6b 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt @@ -159,6 +159,30 @@ enum class SubscriptionPixel( baseName = "m_privacy-pro_app_redirect", type = Count, ), + AUTH_V2_INVALID_REFRESH_TOKEN_DETECTED( + baseName = "m_privacy-pro_auth_invalid_refresh_token_detected", + types = setOf(Count, Daily()), + ), + AUTH_V2_INVALID_REFRESH_TOKEN_SIGNED_OUT( + baseName = "m_privacy-pro_auth_invalid_refresh_token_signed_out", + types = setOf(Count, Daily()), + ), + AUTH_V2_INVALID_REFRESH_TOKEN_RECOVERED( + baseName = "m_privacy-pro_auth_invalid_refresh_token_recovered", + types = setOf(Count, Daily()), + ), + AUTH_V2_MIGRATION_SUCCESS( + baseName = "m_privacy-pro_auth_v2_migration_success", + types = setOf(Count, Daily()), + ), + AUTH_V2_MIGRATION_FAILURE_IO( + baseName = "m_privacy-pro_auth_v2_migration_failure_io", + types = setOf(Count, Daily()), + ), + AUTH_V2_MIGRATION_FAILURE_OTHER( + baseName = "m_privacy-pro_auth_v2_migration_failure_other", + types = setOf(Count, Daily()), + ), ; constructor( diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt index aaf1af6e3249..77774f380051 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt @@ -25,6 +25,12 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ACTIVATE_SUBSC import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_IDTR_CLICK import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_PIR_CLICK import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_RESTORE_PURCHASE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_INVALID_REFRESH_TOKEN_DETECTED +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_INVALID_REFRESH_TOKEN_RECOVERED +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_INVALID_REFRESH_TOKEN_SIGNED_OUT +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_MIGRATION_FAILURE_IO +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_MIGRATION_FAILURE_OTHER +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.AUTH_V2_MIGRATION_SUCCESS import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_RESTORE_PURCHASE_CLICK import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SCREEN_SHOWN import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SUBSCRIBE_CLICK @@ -90,6 +96,12 @@ interface SubscriptionPixelSender { fun reportOnboardingFaqClick() fun reportAddEmailSuccess() fun reportPrivacyProRedirect() + fun reportAuthV2InvalidRefreshTokenDetected() + fun reportAuthV2InvalidRefreshTokenSignedOut() + fun reportAuthV2InvalidRefreshTokenRecovered() + fun reportAuthV2MigrationSuccess() + fun reportAuthV2MigrationFailureIo() + fun reportAuthV2MigrationFailureOther() } @ContributesBinding(AppScope::class) @@ -204,6 +216,30 @@ class SubscriptionPixelSenderImpl @Inject constructor( override fun reportPrivacyProRedirect() = fire(SUBSCRIPTION_PRIVACY_PRO_REDIRECT) + override fun reportAuthV2InvalidRefreshTokenDetected() { + fire(AUTH_V2_INVALID_REFRESH_TOKEN_DETECTED) + } + + override fun reportAuthV2InvalidRefreshTokenSignedOut() { + fire(AUTH_V2_INVALID_REFRESH_TOKEN_SIGNED_OUT) + } + + override fun reportAuthV2InvalidRefreshTokenRecovered() { + fire(AUTH_V2_INVALID_REFRESH_TOKEN_RECOVERED) + } + + override fun reportAuthV2MigrationSuccess() { + fire(AUTH_V2_MIGRATION_SUCCESS) + } + + override fun reportAuthV2MigrationFailureIo() { + fire(AUTH_V2_MIGRATION_FAILURE_IO) + } + + override fun reportAuthV2MigrationFailureOther() { + fire(AUTH_V2_MIGRATION_FAILURE_OTHER) + } + private fun fire(pixel: SubscriptionPixel, params: Map = emptyMap()) { pixel.getPixelNames().forEach { (pixelType, pixelName) -> pixelSender.fire(pixelName = pixelName, type = pixelType, parameters = params) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt index 3c446200b3f8..edf0f790b597 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt @@ -763,6 +763,8 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { assertTrue(result is AccessTokenResult.Success) assertEquals("new access token", (result as AccessTokenResult.Success).accessToken) + verify(pixelSender).reportAuthV2InvalidRefreshTokenDetected() + verify(pixelSender).reportAuthV2InvalidRefreshTokenRecovered() } @Test @@ -784,6 +786,8 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { assertNull(authRepository.getRefreshTokenV2()) assertNull(authRepository.getAccount()) assertNull(authRepository.getSubscription()) + verify(pixelSender).reportAuthV2InvalidRefreshTokenDetected() + verify(pixelSender).reportAuthV2InvalidRefreshTokenSignedOut() } @Test @@ -801,6 +805,7 @@ class RealSubscriptionsManagerTest(private val authApiV2Enabled: Boolean) { assertEquals(FAKE_REFRESH_TOKEN_V2, authRepository.getRefreshTokenV2()?.jwt) assertNull(authRepository.getAccessToken()) assertNull(authRepository.getAuthToken()) + verify(pixelSender).reportAuthV2MigrationSuccess() } @Test