From 718fb37ea09a41a85eebcff779b073f859becdf7 Mon Sep 17 00:00:00 2001 From: Lukasz Macionczyk Date: Wed, 22 Jan 2025 22:02:57 +0100 Subject: [PATCH 1/2] add pixels for monitoring invalid refresh tokens --- .../subscriptions/impl/SubscriptionsManager.kt | 7 ++++++- .../impl/pixels/SubscriptionPixel.kt | 12 ++++++++++++ .../impl/pixels/SubscriptionPixelSender.kt | 18 ++++++++++++++++++ .../impl/RealSubscriptionsManagerTest.kt | 4 ++++ 4 files changed, 40 insertions(+), 1 deletion(-) 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..23687f94c701 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 @@ -537,14 +537,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 } 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..d969a6a51709 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,18 @@ 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()), + ), ; 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..28b90416606b 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,9 @@ 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.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 +93,9 @@ interface SubscriptionPixelSender { fun reportOnboardingFaqClick() fun reportAddEmailSuccess() fun reportPrivacyProRedirect() + fun reportAuthV2InvalidRefreshTokenDetected() + fun reportAuthV2InvalidRefreshTokenSignedOut() + fun reportAuthV2InvalidRefreshTokenRecovered() } @ContributesBinding(AppScope::class) @@ -204,6 +210,18 @@ 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) + } + 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..00ade33fb632 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 From c3c0c74c405b4f45a2f879185868469262d963d0 Mon Sep 17 00:00:00 2001 From: Lukasz Macionczyk Date: Wed, 22 Jan 2025 22:26:57 +0100 Subject: [PATCH 2/2] add pixels to monitor auth v2 migration --- .../impl/SubscriptionsManager.kt | 29 +++++++++++++------ .../impl/pixels/SubscriptionPixel.kt | 12 ++++++++ .../impl/pixels/SubscriptionPixelSender.kt | 18 ++++++++++++ .../impl/RealSubscriptionsManagerTest.kt | 1 + 4 files changed, 51 insertions(+), 9 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 23687f94c701..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 @@ -878,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 d969a6a51709..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 @@ -171,6 +171,18 @@ enum class SubscriptionPixel( 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 28b90416606b..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 @@ -28,6 +28,9 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_R 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 @@ -96,6 +99,9 @@ interface SubscriptionPixelSender { fun reportAuthV2InvalidRefreshTokenDetected() fun reportAuthV2InvalidRefreshTokenSignedOut() fun reportAuthV2InvalidRefreshTokenRecovered() + fun reportAuthV2MigrationSuccess() + fun reportAuthV2MigrationFailureIo() + fun reportAuthV2MigrationFailureOther() } @ContributesBinding(AppScope::class) @@ -222,6 +228,18 @@ class SubscriptionPixelSenderImpl @Inject constructor( 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 00ade33fb632..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 @@ -805,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