Skip to content

Commit 3468ccb

Browse files
In app usage flows now only applies to new users under a new variant. (#893)
1 parent 31e0df1 commit 3468ccb

File tree

28 files changed

+1182
-556
lines changed

28 files changed

+1182
-556
lines changed

app/schemas/com.duckduckgo.app.global.db.AppDatabase/23.json

Lines changed: 746 additions & 0 deletions
Large diffs are not rendered by default.

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,15 @@ class BrowserTabViewModelTest {
21752175
verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA)
21762176
}
21772177

2178+
@Test
2179+
fun whenViewReadyIfDomainSameAsUseOurAppThenPixelSent() = coroutineRule.runBlocking {
2180+
givenUseOurAppSiteSelected()
2181+
2182+
testee.onViewReady()
2183+
2184+
verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED)
2185+
}
2186+
21782187
@Test
21792188
fun whenViewReadyIfDomainIsNotTheSameAsUseOurAppAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking {
21802189
givenUseOurAppSiteIsNotSelected()
@@ -2205,6 +2214,15 @@ class BrowserTabViewModelTest {
22052214
verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA)
22062215
}
22072216

2217+
@Test
2218+
fun whenViewReadyIfDomainIsNotTheSameAsUseOurAppAThenPixelNotSent() = coroutineRule.runBlocking {
2219+
givenUseOurAppSiteIsNotSelected()
2220+
2221+
testee.onViewReady()
2222+
2223+
verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED)
2224+
}
2225+
22082226
@Test
22092227
fun whenPageChangedIfPreviousOneWasNotUseOurAppSiteAfterNotificationSeenThenPixelSent() = coroutineRule.runBlocking {
22102228
givenUseOurAppSiteIsNotSelected()
@@ -2235,6 +2253,15 @@ class BrowserTabViewModelTest {
22352253
verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA)
22362254
}
22372255

2256+
@Test
2257+
fun whenPageChangedIfPreviousOneWasNotUseOurAppSiteThenPixelSent() = coroutineRule.runBlocking {
2258+
givenUseOurAppSiteIsNotSelected()
2259+
2260+
loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true)
2261+
2262+
verify(mockPixel).fire(Pixel.PixelName.UOA_VISITED)
2263+
}
2264+
22382265
@Test
22392266
fun whenPageChangedIfPreviousOneWasUseOurAppSiteAfterNotificationSeenThenPixelNotSent() = coroutineRule.runBlocking {
22402267
givenUseOurAppSiteSelected()
@@ -2266,6 +2293,15 @@ class BrowserTabViewModelTest {
22662293
verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED_AFTER_DELETE_CTA)
22672294
}
22682295

2296+
@Test
2297+
fun whenPageChangedIfPreviousOneWasUseOurAppSiteThenNotSent() = coroutineRule.runBlocking {
2298+
givenUseOurAppSiteSelected()
2299+
2300+
loadUrl(USE_OUR_APP_DOMAIN, isBrowserShowing = true)
2301+
2302+
verify(mockPixel, never()).fire(Pixel.PixelName.UOA_VISITED)
2303+
}
2304+
22692305
private inline fun <reified T : Command> assertCommandIssued(instanceAssertions: T.() -> Unit = {}) {
22702306
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
22712307
val issuedCommand = commandCaptor.allValues.find { it is T }

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ import com.duckduckgo.app.CoroutineTestRule
2323
import com.duckduckgo.app.browser.BrowserViewModel.Command
2424
import com.duckduckgo.app.browser.BrowserViewModel.Command.DisplayMessage
2525
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
26-
import com.duckduckgo.app.cta.db.DismissedCtaDao
27-
import com.duckduckgo.app.cta.model.CtaId
2826
import com.duckduckgo.app.fire.DataClearer
27+
import com.duckduckgo.app.global.events.db.UserEventsStore
2928
import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter
3029
import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions
3130
import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder
3231
import com.duckduckgo.app.global.rating.PromptCount
33-
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
32+
import com.duckduckgo.app.global.useourapp.UseOurAppDetector
33+
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_DOMAIN
3434
import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity
3535
import com.duckduckgo.app.runBlocking
3636
import com.duckduckgo.app.statistics.pixels.Pixel
@@ -85,7 +85,7 @@ class BrowserViewModelTest {
8585
private lateinit var mockPixel: Pixel
8686

8787
@Mock
88-
private lateinit var mockDismissedCtaDao: DismissedCtaDao
88+
private lateinit var mockUserEventsStore: UserEventsStore
8989

9090
private lateinit var testee: BrowserViewModel
9191

@@ -101,7 +101,7 @@ class BrowserViewModelTest {
101101
dataClearer = mockAutomaticDataClearer,
102102
appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter,
103103
appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder,
104-
ctaDao = mockDismissedCtaDao,
104+
useOurAppDetector = UseOurAppDetector(mockUserEventsStore),
105105
dispatchers = coroutinesTestRule.testDispatcherProvider,
106106
pixel = mockPixel
107107
)
@@ -193,23 +193,13 @@ class BrowserViewModelTest {
193193
}
194194

195195
@Test
196-
fun whenOpenShortcutIfUrlIsUseOurAppUrlAndCtaHasBeenSeenThenFirePixel() {
197-
givenUseOurAppCtaHasBeenSeen()
198-
val url = USE_OUR_APP_SHORTCUT_URL
196+
fun whenOpenShortcutIfUrlIsUseOurAppDomainThenFirePixel() {
197+
val url = "http://m.$USE_OUR_APP_DOMAIN"
199198
whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url)
200199
testee.onOpenShortcut(url)
201200
verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED)
202201
}
203202

204-
@Test
205-
fun whenOpenShortcutIfUrlIsUseOurAppUrlAndCtaHasNotBeenSeenThenDoNotFireUseOurAppPixel() {
206-
val url = USE_OUR_APP_SHORTCUT_URL
207-
whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url)
208-
testee.onOpenShortcut(url)
209-
verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_OPENED)
210-
verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_OPENED)
211-
}
212-
213203
@Test
214204
fun whenOpenShortcutIfUrlIsNotUSeOurAppUrlThenFirePixel() {
215205
val url = "example.com"
@@ -218,10 +208,6 @@ class BrowserViewModelTest {
218208
verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_OPENED)
219209
}
220210

221-
private fun givenUseOurAppCtaHasBeenSeen() {
222-
whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP)).thenReturn(true)
223-
}
224-
225211
companion object {
226212
const val TAB_ID = "TAB_ID"
227213
}

app/src/androidTest/java/com/duckduckgo/app/browser/shortcut/ShortcutReceiverTest.kt

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ package com.duckduckgo.app.browser.shortcut
1818

1919
import android.content.Intent
2020
import com.duckduckgo.app.CoroutineTestRule
21-
import com.duckduckgo.app.cta.db.DismissedCtaDao
22-
import com.duckduckgo.app.cta.model.CtaId
2321
import com.duckduckgo.app.global.events.db.UserEventKey
2422
import com.duckduckgo.app.global.events.db.UserEventsStore
25-
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
23+
import com.duckduckgo.app.global.useourapp.UseOurAppDetector
2624
import com.duckduckgo.app.runBlocking
25+
import com.duckduckgo.app.statistics.Variant
26+
import com.duckduckgo.app.statistics.VariantManager
2727
import com.duckduckgo.app.statistics.pixels.Pixel
2828
import com.nhaarman.mockitokotlin2.mock
2929
import com.nhaarman.mockitokotlin2.never
@@ -42,70 +42,89 @@ class ShortcutReceiverTest {
4242

4343
private val mockUserEventsStore: UserEventsStore = mock()
4444
private val mockPixel: Pixel = mock()
45-
private val mockDismissedCtaDao: DismissedCtaDao = mock()
45+
private val mockVariantManager: VariantManager = mock()
4646
private lateinit var testee: ShortcutReceiver
4747

4848
@Before
4949
fun before() {
50-
testee = ShortcutReceiver(mockUserEventsStore, mockDismissedCtaDao, coroutinesTestRule.testDispatcherProvider, mockPixel)
50+
testee = ShortcutReceiver(
51+
mockUserEventsStore,
52+
coroutinesTestRule.testDispatcherProvider,
53+
UseOurAppDetector(mockUserEventsStore),
54+
mockVariantManager,
55+
mockPixel
56+
)
5157
}
5258

5359
@Test
54-
fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenRegisterTimestamp() = coroutinesTestRule.runBlocking {
55-
givenUseOurAppCtaHasBeenSeen()
60+
fun whenIntentReceivedIfUrlIsFromUseOurAppDomainAndVariantIsInAppUsageThenRegisterTimestamp() = coroutinesTestRule.runBlocking {
61+
setInAppUsageVariant()
5662
val intent = Intent()
57-
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL)
63+
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "https://facebook.com")
5864
intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title")
5965
testee.onReceive(null, intent)
6066

6167
verify(mockUserEventsStore).registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)
6268
}
6369

6470
@Test
65-
fun whenIntentReceivedIfUrlIsFromUseOurAppUrlThenFirePixel() {
66-
givenUseOurAppCtaHasBeenSeen()
71+
fun whenIntentReceivedIfUrlIsFromUseOurAppDomainAndVariantIsNotInAppUsageThenDoNotRegisterTimestamp() = coroutinesTestRule.runBlocking {
72+
setDefaultVariant()
6773
val intent = Intent()
68-
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL)
74+
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "https://facebook.com")
6975
intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title")
7076
testee.onReceive(null, intent)
7177

72-
verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED)
78+
verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)
7379
}
7480

7581
@Test
76-
fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenDoNotRegisterEvent() = coroutinesTestRule.runBlocking {
77-
givenUseOurAppCtaHasBeenSeen()
82+
fun whenIntentReceivedIfUrlContainsUseOurAppDomainThenFirePixel() {
83+
setDefaultVariant()
7884
val intent = Intent()
79-
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com")
85+
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "https://facebook.com")
8086
intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title")
8187
testee.onReceive(null, intent)
8288

83-
verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)
89+
verify(mockPixel).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED)
8490
}
8591

8692
@Test
87-
fun whenIntentReceivedIfUrlIsNotFromUseOurAppUrlThenFireShortcutAddedPixel() {
93+
fun whenIntentReceivedIfUrlIsNotFromUseOurAppDomainThenDoNotRegisterEvent() = coroutinesTestRule.runBlocking {
94+
setDefaultVariant()
8895
val intent = Intent()
8996
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com")
9097
intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title")
9198
testee.onReceive(null, intent)
9299

93-
verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_ADDED)
100+
verify(mockUserEventsStore, never()).registerUserEvent(UserEventKey.USE_OUR_APP_SHORTCUT_ADDED)
94101
}
95102

96103
@Test
97-
fun whenIntentReceivedAndCtaNotSeenIfUrlIsFromUseOurAppUrlThenDoNotFireUseOurAppPixelAndFireShortcutPixel() = coroutinesTestRule.runBlocking {
104+
fun whenIntentReceivedIfUrlIsNotFromUseOurAppDomainThenFireShortcutAddedPixel() {
105+
setDefaultVariant()
98106
val intent = Intent()
99-
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, USE_OUR_APP_SHORTCUT_URL)
100-
107+
intent.putExtra(ShortcutBuilder.SHORTCUT_URL_ARG, "www.example.com")
101108
intent.putExtra(ShortcutBuilder.SHORTCUT_TITLE_ARG, "Title")
102109
testee.onReceive(null, intent)
103110

104-
verify(mockPixel, never()).fire(Pixel.PixelName.USE_OUR_APP_SHORTCUT_ADDED)
105111
verify(mockPixel).fire(Pixel.PixelName.SHORTCUT_ADDED)
106112
}
107113

108-
private fun givenUseOurAppCtaHasBeenSeen() {
109-
whenever(mockDismissedCtaDao.exists(CtaId.USE_OUR_APP)).thenReturn(true)
114+
private fun setDefaultVariant() {
115+
whenever(mockVariantManager.getVariant()).thenReturn(VariantManager.DEFAULT_VARIANT)
116+
}
117+
118+
private fun setInAppUsageVariant() {
119+
whenever(mockVariantManager.getVariant()).thenReturn(
120+
Variant(
121+
"test",
122+
features = listOf(
123+
VariantManager.VariantFeature.InAppUsage,
124+
VariantManager.VariantFeature.RemoveDay1AndDay3Notifications,
125+
VariantManager.VariantFeature.KillOnboarding
126+
),
127+
filterBy = { true })
128+
)
110129
}
111130
}

app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt

Lines changed: 11 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import com.duckduckgo.app.blockingObserve
3232
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
3333
import com.duckduckgo.app.global.exception.UncaughtExceptionEntity
3434
import com.duckduckgo.app.global.exception.UncaughtExceptionSource
35-
import com.duckduckgo.app.global.useourapp.MigrationManager
3635
import com.duckduckgo.app.onboarding.store.AppStage
3736
import com.duckduckgo.app.runBlocking
3837
import com.duckduckgo.app.settings.db.SettingsDataStore
@@ -63,10 +62,9 @@ class AppDatabaseTest {
6362
private val context = mock<Context>()
6463
private val mockSettingsDataStore = mock<SettingsDataStore>()
6564
private val mockAddToHomeCapabilityDetector = mock<AddToHomeCapabilityDetector>()
66-
private val mockUseOurAppMigrationManager = mock<MigrationManager>()
6765

6866
private val migrationsProvider: MigrationsProvider =
69-
MigrationsProvider(context, mockSettingsDataStore, mockAddToHomeCapabilityDetector, mockUseOurAppMigrationManager)
67+
MigrationsProvider(context, mockSettingsDataStore, mockAddToHomeCapabilityDetector)
7068

7169
@Before
7270
fun setup() {
@@ -234,76 +232,34 @@ class AppDatabaseTest {
234232
}
235233

236234
@Test
237-
fun whenMigratingFromVersion21To22IfUserIsEstablishedAndConditionsAreMetThenMigrateToNotification() {
238-
testHelper.createDatabase(TEST_DB_NAME, 21).use {
239-
givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = true)
240-
givenUserStageIs(it, AppStage.ESTABLISHED)
241-
242-
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22)
243-
val stage = getUserStage(it)
244-
245-
assertEquals(AppStage.USE_OUR_APP_NOTIFICATION.name, stage)
246-
}
247-
}
248-
249-
@Test
250-
fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHideTipsIsTrueThenDoNotMigrateToNotification() {
251-
testHelper.createDatabase(TEST_DB_NAME, 21).use {
252-
givenUseOurAppStateIs(canMigrate = true, hideTips = true, canAddToHome = true)
253-
givenUserStageIs(it, AppStage.ESTABLISHED)
254-
255-
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22)
256-
val stage = getUserStage(it)
257-
258-
assertEquals(AppStage.ESTABLISHED.name, stage)
259-
}
235+
fun whenMigratingFromVersion22To23ThenValidationSucceeds() {
236+
createDatabaseAndMigrate(22, 23, migrationsProvider.MIGRATION_22_TO_23)
260237
}
261238

262239
@Test
263-
fun whenMigratingFromVersion21To22IfUserIsEstablishedAndHomeShortcutNotSupportedThenDoNotMigrateToNotification() {
264-
testHelper.createDatabase(TEST_DB_NAME, 21).use {
265-
givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = false)
266-
givenUserStageIs(it, AppStage.ESTABLISHED)
240+
fun whenMigratingFromVersion22To23IfUserStageIsUseOurAppNotificationThenMigrateToEstablished() {
241+
testHelper.createDatabase(TEST_DB_NAME, 22).use {
242+
givenUserStageIs(it, AppStage.USE_OUR_APP_NOTIFICATION)
267243

268-
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22)
244+
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 23, true, migrationsProvider.MIGRATION_22_TO_23)
269245
val stage = getUserStage(it)
270246

271247
assertEquals(AppStage.ESTABLISHED.name, stage)
272248
}
273249
}
274250

275251
@Test
276-
fun whenMigratingFromVersion21To22IfUserIsNotEstablishedThenDoNotMigrateToNotification() {
277-
testHelper.createDatabase(TEST_DB_NAME, 21).use {
278-
givenUseOurAppStateIs(canMigrate = true, hideTips = false, canAddToHome = false)
252+
fun whenMigratingFromVersion22To23IfUserStageIsNotUseOurAppNotificationThenDoNotMigrateToEstablished() {
253+
testHelper.createDatabase(TEST_DB_NAME, 22).use {
279254
givenUserStageIs(it, AppStage.ESTABLISHED)
280255

281-
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22)
256+
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 23, true, migrationsProvider.MIGRATION_22_TO_23)
282257
val stage = getUserStage(it)
283258

284259
assertEquals(AppStage.ESTABLISHED.name, stage)
285260
}
286261
}
287262

288-
@Test
289-
fun whenMigratingFromVersion21To22IfShouldNotRunMigrationThenDoNotMigrateToNotification2() {
290-
testHelper.createDatabase(TEST_DB_NAME, 21).use {
291-
givenUseOurAppStateIs(canMigrate = false)
292-
givenUserStageIs(it, AppStage.ESTABLISHED)
293-
294-
testHelper.runMigrationsAndValidate(TEST_DB_NAME, 22, true, migrationsProvider.MIGRATION_21_TO_22)
295-
val stage = getUserStage(it)
296-
297-
assertEquals(AppStage.ESTABLISHED.name, stage)
298-
}
299-
}
300-
301-
private fun givenUseOurAppStateIs(canMigrate: Boolean = true, hideTips: Boolean = false, canAddToHome: Boolean = true) {
302-
whenever(mockUseOurAppMigrationManager.shouldRunMigration()).thenReturn(canMigrate)
303-
whenever(mockSettingsDataStore.hideTips).thenReturn(hideTips)
304-
whenever(mockAddToHomeCapabilityDetector.isAddToHomeSupported()).thenReturn(canAddToHome)
305-
}
306-
307263
private fun givenUserStageIs(database: SupportSQLiteDatabase, appStage: AppStage) {
308264
val values: ContentValues = ContentValues().apply {
309265
put("key", 1)
@@ -316,7 +272,7 @@ class AppDatabaseTest {
316272
}
317273

318274
private fun getUserStage(database: SupportSQLiteDatabase): String {
319-
var stage = ""
275+
var stage: String
320276

321277
database.query("SELECT appStage from userStage limit 1").apply {
322278
moveToFirst()

0 commit comments

Comments
 (0)