Skip to content

Commit 177ea17

Browse files
Add "flow_type" dimension to upsell telemetry [VPNAND-1529].
1 parent eeeae5a commit 177ea17

File tree

11 files changed

+100
-33
lines changed

11 files changed

+100
-33
lines changed

app/src/google/java/com/protonvpn/android/ui/planupgrade/PaymentPanelFragment.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class PaymentPanelFragment : BaseBillingIAPFragment(0) {
123123
is BillingCommonViewModel.State.Success.TokenCreated -> {
124124
}
125125
is BillingCommonViewModel.State.Success.SubscriptionCreated -> {
126-
viewModel.onPurchaseSuccess()
126+
viewModel.onPurchaseSuccess(UpgradeFlowType.ONE_CLICK)
127127
}
128128
}
129129
}.launchIn(viewLifecycleOwner.lifecycleScope)
@@ -179,7 +179,7 @@ class PaymentPanelFragment : BaseBillingIAPFragment(0) {
179179
viewLifecycleOwner.lifecycleScope.launch {
180180
val billingInput = viewModel.getBillingInput(resources)
181181
if (billingInput != null) {
182-
viewModel.onPaymentStarted()
182+
viewModel.onPaymentStarted(UpgradeFlowType.ONE_CLICK)
183183
pay(billingInput)
184184
} else {
185185
onError(null, null)

app/src/google/java/com/protonvpn/android/ui/planupgrade/UpgradeDialogViewModel.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,11 @@ class UpgradeDialogViewModel(
156156
)
157157
}
158158

159-
fun onPurchaseSuccess() {
159+
fun onPurchaseSuccess(upgradeFlowType: UpgradeFlowType) {
160160
state.value = State.PurchaseSuccess(
161161
newPlanName = loadedPlan.name,
162-
newPlanDisplayName = loadedPlan.displayName
162+
newPlanDisplayName = loadedPlan.displayName,
163+
upgradeFlowType = upgradeFlowType
163164
)
164165
}
165166

app/src/main/java/com/protonvpn/android/telemetry/UpgradeTelemetry.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.protonvpn.android.appconfig.GetFeatureFlags
2323
import com.protonvpn.android.auth.usecase.CurrentUser
2424
import com.protonvpn.android.di.WallClock
2525
import com.protonvpn.android.telemetry.CommonDimensions.Companion.NO_VALUE
26+
import com.protonvpn.android.ui.planupgrade.UpgradeFlowType
2627
import kotlinx.coroutines.CoroutineScope
2728
import kotlinx.coroutines.channels.Channel
2829
import kotlinx.coroutines.channels.consumeEach
@@ -83,21 +84,21 @@ class UpgradeTelemetry @Inject constructor(
8384
}
8485
}
8586

86-
fun onUpgradeAttempt() {
87+
fun onUpgradeAttempt(flowType: UpgradeFlowType) {
8788
serialExecutor.trySend {
8889
currentDimensions?.let { currentDimensions ->
89-
sendEvent("upsell_upgrade_attempt", currentDimensions)
90+
sendEvent("upsell_upgrade_attempt", currentDimensions.withFlowType(flowType))
9091
}
9192
}
9293
}
9394

94-
fun onUpgradeSuccess(newPlanId: String?) {
95+
fun onUpgradeSuccess(newPlanId: String?, flowType: UpgradeFlowType) {
9596
serialExecutor.trySend {
9697
currentDimensions?.let { currentDimensions ->
9798
val upgradedPlan = newPlanId ?: NO_VALUE
9899
val dimensions = currentDimensions + ("upgraded_user_plan" to upgradedPlan)
99100
currentUpgradeFlow = null
100-
sendEvent("upsell_success", dimensions)
101+
sendEvent("upsell_success", dimensions.withFlowType(flowType))
101102
}
102103
}
103104
}
@@ -125,6 +126,9 @@ class UpgradeTelemetry @Inject constructor(
125126
}
126127
}
127128

129+
private fun Map<String, String>.withFlowType(flowType: UpgradeFlowType) =
130+
this + ("flow_type" to flowType.toStatsName())
131+
128132
private fun accountCreationBucket(timeSinceCreation: Duration?): String {
129133
if (timeSinceCreation == null) return NO_VALUE
130134

app/src/main/java/com/protonvpn/android/ui/home/countries/CountryListFragment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.protonvpn.android.telemetry.UpgradeTelemetry
3636
import com.protonvpn.android.ui.HeaderViewHolder
3737
import com.protonvpn.android.ui.home.FreeConnectionsInfoActivity
3838
import com.protonvpn.android.ui.planupgrade.UpgradeDialogActivity
39+
import com.protonvpn.android.ui.planupgrade.UpgradeFlowType
3940
import com.protonvpn.android.ui.planupgrade.UpgradePlusCountriesHighlightsFragment
4041
import com.protonvpn.android.ui.promooffers.PromoOfferButtonActions
4142
import com.protonvpn.android.utils.AndroidUtils.launchActivity
@@ -129,7 +130,7 @@ class CountryListFragment : Fragment(R.layout.fragment_country_list), NetworkLoa
129130

130131
if (url != null) { // It's not null on correctly defined notifications.
131132
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.PROMO_OFFER, model.reference)
132-
upgradeTelemetry.onUpgradeAttempt()
133+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.EXTERNAL)
133134
requireActivity().openUrl(url)
134135
}
135136
}

app/src/main/java/com/protonvpn/android/ui/planupgrade/CommonUpgradeDialogViewModel.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ abstract class CommonUpgradeDialogViewModel(
6868
object PlansFallback : State() // Conditions for short flow were not met, start normal account flow
6969
data class PurchaseSuccess(
7070
val newPlanName: String,
71-
val newPlanDisplayName: String
71+
val newPlanDisplayName: String,
72+
val upgradeFlowType: UpgradeFlowType
7273
) : State()
7374
}
7475
val state = MutableStateFlow<State>(State.Initializing)
@@ -84,7 +85,11 @@ abstract class CommonUpgradeDialogViewModel(
8485
plansOrchestrator.onUpgradeResult { result ->
8586
state.update { current ->
8687
if (result != null && result.billingResult.subscriptionCreated) {
87-
State.PurchaseSuccess(newPlanName = result.planId, newPlanDisplayName = result.planDisplayName)
88+
State.PurchaseSuccess(
89+
newPlanName = result.planId,
90+
newPlanDisplayName = result.planDisplayName,
91+
upgradeFlowType = UpgradeFlowType.REGULAR
92+
)
8893
} else if (current is State.PurchaseReady) {
8994
current.copy(inProgress = false)
9095
} else {
@@ -94,14 +99,14 @@ abstract class CommonUpgradeDialogViewModel(
9499
}
95100
}
96101

97-
fun onPaymentStarted() {
102+
fun onPaymentStarted(upgradeFlowType: UpgradeFlowType) {
98103
state.update { if (it is State.PurchaseReady) it.copy(inProgress = true) else it }
99-
upgradeTelemetry.onUpgradeAttempt()
104+
upgradeTelemetry.onUpgradeAttempt(upgradeFlowType)
100105
}
101106

102107
fun onStartFallbackUpgrade() = viewModelScope.launch {
103108
userId.first()?.let { userId ->
104-
onPaymentStarted()
109+
onPaymentStarted(UpgradeFlowType.REGULAR)
105110
plansOrchestrator.startUpgradeWorkflow(userId)
106111
}
107112
}

app/src/main/java/com/protonvpn/android/ui/planupgrade/ShowUpgradeSuccess.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ class ShowUpgradeSuccess constructor(
7373
showPlanUpgradeSuccess(
7474
activity,
7575
upgradedUser.userTierName,
76-
refreshVpnInfo = false
76+
refreshVpnInfo = false,
77+
upgradeFlowType = UpgradeFlowType.EXTERNAL
7778
)
7879
} else {
7980
doNotShowForPlan = ""
@@ -86,9 +87,11 @@ class ShowUpgradeSuccess constructor(
8687
return currentUser.vpnUser()?.userId == upgraded.userId && !upgraded.isFreeUser && doNotShowForPlan != upgraded.userTierName
8788
}
8889

89-
fun showPlanUpgradeSuccess(context: Context, newPlan: String, refreshVpnInfo: Boolean) {
90+
fun showPlanUpgradeSuccess(
91+
context: Context, newPlan: String, refreshVpnInfo: Boolean, upgradeFlowType: UpgradeFlowType
92+
) {
9093
doNotShowForPlan = newPlan
91-
upgradeTelemetry.onUpgradeSuccess(newPlan)
94+
upgradeTelemetry.onUpgradeSuccess(newPlan, upgradeFlowType)
9295
startUpgradeActivity(context, newPlan, refreshVpnInfo)
9396
}
9497
}

app/src/main/java/com/protonvpn/android/ui/planupgrade/UpgradeDialogActivity.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ open class UpgradeDialogActivity : BaseActivityV2() {
7474
is CommonUpgradeDialogViewModel.State.PlanLoaded -> {}
7575
CommonUpgradeDialogViewModel.State.PlansFallback -> {}
7676
is CommonUpgradeDialogViewModel.State.PurchaseSuccess -> {
77-
showUpgradeSuccess.showPlanUpgradeSuccess(this, state.newPlanName, refreshVpnInfo = true)
77+
showUpgradeSuccess.showPlanUpgradeSuccess(
78+
this,
79+
state.newPlanName,
80+
refreshVpnInfo = true,
81+
upgradeFlowType = state.upgradeFlowType
82+
)
7883
setResult(Activity.RESULT_OK)
7984
finish()
8085
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2023. Proton AG
3+
*
4+
* This file is part of ProtonVPN.
5+
*
6+
* ProtonVPN is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* ProtonVPN is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
package com.protonvpn.android.ui.planupgrade
21+
22+
enum class UpgradeFlowType {
23+
// Note: these names are used in telemetry.
24+
REGULAR,
25+
ONE_CLICK,
26+
EXTERNAL;
27+
28+
fun toStatsName(): String = this.name.lowercase()
29+
}

app/src/main/java/com/protonvpn/android/ui/promooffers/PromoOfferViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.protonvpn.android.appconfig.ApiNotificationManager
2525
import com.protonvpn.android.appconfig.ApiNotificationOfferPanel
2626
import com.protonvpn.android.telemetry.UpgradeSource
2727
import com.protonvpn.android.telemetry.UpgradeTelemetry
28+
import com.protonvpn.android.ui.planupgrade.UpgradeFlowType
2829
import dagger.hilt.android.lifecycle.HiltViewModel
2930
import kotlinx.coroutines.flow.MutableSharedFlow
3031
import kotlinx.coroutines.flow.MutableStateFlow
@@ -57,7 +58,7 @@ class PromoOfferViewModel @Inject constructor(
5758
fun onOpenOfferClicked() {
5859
val button = currentPanel.button ?: return
5960

60-
upgradeTelemetry.onUpgradeAttempt()
61+
upgradeTelemetry.onUpgradeAttempt(flowType = UpgradeFlowType.EXTERNAL)
6162
viewModelScope.launch {
6263
isLoading.value = true
6364
val urlToOpen = promoOfferButtonActions.getButtonUrl(button)

app/src/test/java/com/protonvpn/app/telemetry/UpgradeTelemetryTests.kt

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.protonvpn.android.telemetry.Telemetry
2727
import com.protonvpn.android.telemetry.UpgradeSource
2828
import com.protonvpn.android.telemetry.UpgradeTelemetry
2929
import com.protonvpn.android.ui.home.ServerListUpdaterPrefs
30+
import com.protonvpn.android.ui.planupgrade.UpgradeFlowType
3031
import com.protonvpn.android.vpn.VpnStateMonitor
3132
import com.protonvpn.test.shared.MockSharedPreferencesProvider
3233
import com.protonvpn.test.shared.TestCurrentUserProvider
@@ -114,11 +115,27 @@ class UpgradeTelemetryTests {
114115
}
115116
}
116117

118+
@Test
119+
fun `flow_type dimension`() = testScope.runTest {
120+
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.COUNTRIES, "ref")
121+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.ONE_CLICK)
122+
123+
verify {
124+
mockTelemetry.event(UPSELL_GROUP, "upsell_display", emptyMap(), any())
125+
mockTelemetry.event(
126+
UPSELL_GROUP,
127+
"upsell_upgrade_attempt",
128+
emptyMap(),
129+
withArg { assertEquals( "one_click", it["flow_type"]) }
130+
)
131+
}
132+
}
133+
117134
@Test
118135
fun `modal_source is carried over to subsequent events`() = testScope.runTest {
119136
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.NETSHIELD)
120-
upgradeTelemetry.onUpgradeAttempt()
121-
upgradeTelemetry.onUpgradeSuccess("new_plan")
137+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.REGULAR)
138+
upgradeTelemetry.onUpgradeSuccess("new_plan", UpgradeFlowType.REGULAR)
122139

123140
verify {
124141
listOf("upsell_display", "upsell_upgrade_attempt", "upsell_success").forEach { event ->
@@ -136,10 +153,10 @@ class UpgradeTelemetryTests {
136153
fun `when new flow starts it overrides the previous modal_source`() = testScope.runTest {
137154
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.NETSHIELD)
138155
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.MODERATE_NAT)
139-
upgradeTelemetry.onUpgradeAttempt()
156+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.REGULAR)
140157
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.PROFILES)
141-
upgradeTelemetry.onUpgradeAttempt()
142-
upgradeTelemetry.onUpgradeSuccess("new_plan")
158+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.REGULAR)
159+
upgradeTelemetry.onUpgradeSuccess("new_plan", UpgradeFlowType.REGULAR)
143160

144161
verify {
145162
listOf("upsell_display", "upsell_upgrade_attempt", "upsell_success").forEach { event ->
@@ -156,8 +173,8 @@ class UpgradeTelemetryTests {
156173
@Test
157174
fun `on success both old and new plan is reported`() = testScope.runTest {
158175
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.MODERATE_NAT)
159-
upgradeTelemetry.onUpgradeAttempt()
160-
upgradeTelemetry.onUpgradeSuccess("new_plan")
176+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.REGULAR)
177+
upgradeTelemetry.onUpgradeSuccess("new_plan", UpgradeFlowType.REGULAR)
161178

162179
verify {
163180
mockTelemetry.event(
@@ -199,9 +216,9 @@ class UpgradeTelemetryTests {
199216
@Test
200217
fun `upgrade more than 10 minutes after first event is ignored`() = testScope.runTest {
201218
upgradeTelemetry.onUpgradeFlowStarted(UpgradeSource.MODERATE_NAT)
202-
upgradeTelemetry.onUpgradeAttempt()
219+
upgradeTelemetry.onUpgradeAttempt(UpgradeFlowType.REGULAR)
203220
fakeTime = 10.minutes.inWholeMilliseconds + 1
204-
upgradeTelemetry.onUpgradeSuccess("new_plan")
221+
upgradeTelemetry.onUpgradeSuccess("new_plan", UpgradeFlowType.REGULAR)
205222

206223
verify(exactly = 0) {
207224
mockTelemetry.event(UPSELL_GROUP, "upsell_success", any(), any())

app/src/test/java/com/protonvpn/app/ui/planupgrade/UpgradeDialogViewModelTests.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package com.protonvpn.app.ui.planupgrade
2222
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
2323
import com.protonvpn.android.ui.planupgrade.CommonUpgradeDialogViewModel
2424
import com.protonvpn.android.ui.planupgrade.UpgradeDialogViewModel
25+
import com.protonvpn.android.ui.planupgrade.UpgradeFlowType
2526
import com.protonvpn.android.ui.planupgrade.usecase.CycleInfo
2627
import com.protonvpn.android.ui.planupgrade.usecase.GiapPlanInfo
2728
import com.protonvpn.android.utils.formatPrice
@@ -105,18 +106,18 @@ class UpgradeDialogViewModelTests {
105106
)
106107
Assert.assertTrue((viewModel.state.value as? CommonUpgradeDialogViewModel.State.PurchaseReady)?.inProgress == false)
107108

108-
viewModel.onPaymentStarted()
109+
viewModel.onPaymentStarted(UpgradeFlowType.REGULAR)
109110
Assert.assertTrue((viewModel.state.value as? CommonUpgradeDialogViewModel.State.PurchaseReady)?.inProgress == true)
110111

111112
// Fail before succeeding
112113
viewModel.onErrorInFragment()
113114
Assert.assertTrue((viewModel.state.value as? CommonUpgradeDialogViewModel.State.PurchaseReady)?.inProgress == false)
114115

115116
// Try again and succeed
116-
viewModel.onPaymentStarted()
117-
viewModel.onPurchaseSuccess()
117+
viewModel.onPaymentStarted(UpgradeFlowType.ONE_CLICK)
118+
viewModel.onPurchaseSuccess(UpgradeFlowType.ONE_CLICK)
118119
Assert.assertEquals(
119-
CommonUpgradeDialogViewModel.State.PurchaseSuccess("myplan", "My Plan"),
120+
CommonUpgradeDialogViewModel.State.PurchaseSuccess("myplan", "My Plan", UpgradeFlowType.ONE_CLICK),
120121
viewModel.state.value
121122
)
122123
}
@@ -175,4 +176,4 @@ private fun priceDetails(price: Int) = GoogleProductDetails(
175176
priceAmountMicros = price * 1000000L,
176177
currency = "USD",
177178
formattedPriceAndCurrency = formatPrice(price.toDouble(), "USD")
178-
)
179+
)

0 commit comments

Comments
 (0)