Skip to content

Commit

Permalink
Auth V2 pixels (#5518)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lmac012 authored Jan 23, 2025
1 parent caec31e commit e87bbe9
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<String, String> = emptyMap()) {
pixel.getPixelNames().forEach { (pixelType, pixelName) ->
pixelSender.fire(pixelName = pixelName, type = pixelType, parameters = params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit e87bbe9

Please sign in to comment.