Skip to content

Commit 503911f

Browse files
authored
Add Aura experiment (#5343)
Task/Issue URL: https://app.asana.com/0/488551667048375/1208893765398547/f ### Description Sets the ATB variant and origin for Aura installs in order to measure retention. ### Steps to test this PR _Point at the JSON Blob linked in the task_ _Feature enabled and new user Aura install_ - [x] Check out this branch - [x] Change `installationSourceModern()` in `RealInstallSourceExtractor` to return an Aura package (e.g. `com.sec.android.app.samsungapps`) - [x] On a fresh emulator, freshly install the app - [x] Filter logcat by `Initialized ATB` - [x] Verify that the variant is `mq` - [x] Filter logcat by `m_android_install` - [x] Verify that `origin=funnel_app_aurapaid_android` - [x] Verify that `reinstall=false` _Feature enabled and returning user Aura install_ - [x] On the same emulator, uninstall and freshly install the app - [x] Filter logcat by `Initialized ATB` - [x] Verify that the variant is `mq` - [x] Filter logcat by `m_android_install` - [x] Verify that `origin=funnel_app_aurapaid_android` - [x] Verify that `reinstall=true` _Feature disabled and new user Aura install_ - [x] Disable `auraExperiment` in the config - [x] On a fresh emulator, freshly install the app - [x] Filter logcat by `Initialized ATB` - [x] Verify that the variant is set normally (Not `mq`) - [x] Filter logcat by `m_android_install` - [x] Verify that `origin=funnel_app_aurapaid_android` is **not** sent - [x] Verify that `reinstall=false` _Feature disabled and returning user Aura install_ - [x] On the same emulator, uninstall and freshly install the app - [x] Filter logcat by `Initialized ATB` - [x] Verify that the variant is set normally (Not `mq`) - [x] Filter logcat by `m_android_install` - [x] Verify that `origin=funnel_app_aurapaid_android` is **not** sent - [x] Verify that `reinstall=true` _Feature enabled and new user install (Not Aura)_ - [x] Re-enable `auraExperiment` in the config - [x] Change `installationSourceModern()` in `RealInstallSourceExtractor` to return something else (e.g. `com.example`) - [x] On a fresh emulator, freshly install the app - [x] Filter logcat by `Initialized ATB` - [x] Verify that the variant is set normally (Not `mq`) - [x] Filter logcat by `m_android_install` - [x] Verify that `origin=funnel_app_aurapaid_android` is **not** sent - [x] Verify that `reinstall=false` _Feature enabled and returning user install (Not Aura)_ - [x] On the same emulator, uninstall and freshly install the app - [x] Filter logcat by `Initialized ATB` - [x] Verify that the variant is set normally (Not `mq`) - [x] Filter logcat by `m_android_install` - [x] Verify that `origin=funnel_app_aurapaid_android` is **not** sent - [x] Verify that `reinstall=true`
1 parent 026f49f commit 503911f

File tree

28 files changed

+565
-238
lines changed

28 files changed

+565
-238
lines changed

app-build-config/app-build-config-api/src/main/java/com/duckduckgo/appbuildconfig/api/AppBuildConfig.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ interface AppBuildConfig {
3838
* You should call [variantName] in a background thread
3939
*/
4040
val variantName: String?
41+
42+
/**
43+
* @return `true` if the user re-installed the app, `false` otherwise
44+
*/
45+
suspend fun isAppReinstall(): Boolean
4146
}
4247

4348
enum class BuildFlavor {

app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import com.duckduckgo.app.statistics.store.StatisticsDataStore
3131
import com.duckduckgo.common.utils.DispatcherProvider
3232
import com.duckduckgo.common.utils.device.ContextDeviceInfo
3333
import com.duckduckgo.common.utils.device.DeviceInfo
34-
import com.duckduckgo.di.DaggerSet
34+
import com.duckduckgo.common.utils.plugins.PluginPoint
3535
import com.duckduckgo.di.scopes.AppScope
3636
import com.squareup.anvil.annotations.ContributesTo
3737
import dagger.Module
@@ -115,7 +115,7 @@ class StubStatisticsModule {
115115
@AppCoroutineScope appCoroutineScope: CoroutineScope,
116116
statisticsDataStore: StatisticsDataStore,
117117
statisticsUpdater: StatisticsUpdater,
118-
listeners: DaggerSet<AtbInitializerListener>,
118+
listeners: PluginPoint<AtbInitializerListener>,
119119
dispatcherProvider: DispatcherProvider,
120120
): MainProcessLifecycleObserver {
121121
return AtbInitializer(appCoroutineScope, statisticsDataStore, statisticsUpdater, listeners, dispatcherProvider)

app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,34 @@
1717
package com.duckduckgo.app.buildconfig
1818

1919
import android.os.Build
20+
import android.os.Environment
21+
import androidx.core.content.edit
2022
import com.duckduckgo.app.browser.BuildConfig
2123
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2224
import com.duckduckgo.appbuildconfig.api.BuildFlavor
25+
import com.duckduckgo.common.utils.DispatcherProvider
26+
import com.duckduckgo.data.store.api.SharedPreferencesProvider
2327
import com.duckduckgo.di.scopes.AppScope
2428
import com.duckduckgo.experiments.api.VariantManager
2529
import com.squareup.anvil.annotations.ContributesBinding
2630
import dagger.Lazy
31+
import java.io.File
2732
import java.lang.IllegalStateException
2833
import java.util.*
2934
import javax.inject.Inject
35+
import kotlinx.coroutines.withContext
36+
import timber.log.Timber
3037

3138
@ContributesBinding(AppScope::class)
3239
class RealAppBuildConfig @Inject constructor(
3340
private val variantManager: Lazy<VariantManager>, // break any possible DI dependency cycle
41+
private val dispatcherProvider: DispatcherProvider,
42+
private val sharedPreferencesProvider: SharedPreferencesProvider,
3443
) : AppBuildConfig {
44+
private val preferences by lazy {
45+
sharedPreferencesProvider.getSharedPreferences("com.duckduckgo.app.buildconfig.cache", false, false)
46+
}
47+
3548
override val isDebug: Boolean = BuildConfig.DEBUG
3649
override val applicationId: String = BuildConfig.APPLICATION_ID
3750
override val buildType: String = BuildConfig.BUILD_TYPE
@@ -65,6 +78,59 @@ class RealAppBuildConfig @Inject constructor(
6578
override val variantName: String?
6679
get() = variantManager.get().getVariantKey()
6780

81+
override suspend fun isAppReinstall(): Boolean = withContext(dispatcherProvider.io()) {
82+
return@withContext kotlin.runCatching {
83+
if (sdkInt < 30) {
84+
return@withContext false
85+
}
86+
87+
if (preferences.contains(APP_REINSTALLED_KEY)) {
88+
return@withContext preferences.getBoolean(APP_REINSTALLED_KEY, false)
89+
}
90+
91+
val downloadDirectory = getDownloadsDirectory()
92+
val ddgDirectoryExists = (downloadDirectory.list()?.asList() ?: emptyList()).contains(DDG_DOWNLOADS_DIRECTORY)
93+
val appReinstallValue = if (!ddgDirectoryExists) {
94+
createNewDirectory(DDG_DOWNLOADS_DIRECTORY)
95+
// this is a new install
96+
false
97+
} else {
98+
true
99+
}
100+
preferences.edit(commit = true) { putBoolean(APP_REINSTALLED_KEY, appReinstallValue) }
101+
return@withContext appReinstallValue
102+
}.getOrDefault(false)
103+
}
104+
68105
override val buildDateTimeMillis: Long
69106
get() = BuildConfig.BUILD_DATE_MILLIS
107+
108+
private fun getDownloadsDirectory(): File {
109+
val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
110+
if (!downloadDirectory.exists()) {
111+
Timber.i("Download directory doesn't exist; trying to create it. %s", downloadDirectory.absolutePath)
112+
downloadDirectory.mkdirs()
113+
}
114+
return downloadDirectory
115+
}
116+
117+
private fun createNewDirectory(directoryName: String) {
118+
val directory = File(getDownloadsDirectory(), directoryName)
119+
val success = directory.mkdirs()
120+
Timber.i("Directory creation success: %s", success)
121+
if (!success) {
122+
Timber.e("Directory creation failed")
123+
kotlin.runCatching {
124+
val directoryCreationSuccess = directory.createNewFile()
125+
Timber.i("File creation success: %s", directoryCreationSuccess)
126+
}.onFailure {
127+
Timber.w("Failed to create file: %s", it.message)
128+
}
129+
}
130+
}
131+
132+
companion object {
133+
private const val APP_REINSTALLED_KEY = "appReinstalled"
134+
private const val DDG_DOWNLOADS_DIRECTORY = "DuckDuckGo"
135+
}
70136
}

app/src/main/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParams.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616

1717
package com.duckduckgo.app.pixels.campaign.params
1818

19-
import com.duckduckgo.app.statistics.store.StatisticsDataStore
19+
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2020
import com.duckduckgo.di.scopes.AppScope
2121
import com.squareup.anvil.annotations.ContributesMultibinding
2222
import javax.inject.Inject
2323

2424
@ContributesMultibinding(AppScope::class)
2525
class ReinstallAdditionalPixelParamPlugin @Inject constructor(
26-
private val statisticsDataStore: StatisticsDataStore,
26+
private val appBuildConfig: AppBuildConfig,
2727
) : AdditionalPixelParamPlugin {
2828
override suspend fun params(): Pair<String, String> = Pair(
2929
"isReinstall",
30-
"${statisticsDataStore.variant == "ru"}",
30+
"${appBuildConfig.isAppReinstall()}",
3131
)
3232
}

app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ package com.duckduckgo.app.referral
1919
import android.content.Context
2020
import android.content.SharedPreferences
2121
import androidx.core.content.edit
22+
import com.duckduckgo.app.di.AppCoroutineScope
23+
import com.duckduckgo.browser.api.referrer.AppReferrer
24+
import com.duckduckgo.common.utils.DispatcherProvider
2225
import com.duckduckgo.di.scopes.AppScope
2326
import com.squareup.anvil.annotations.ContributesBinding
2427
import dagger.SingleInstanceIn
2528
import javax.inject.Inject
29+
import kotlinx.coroutines.CoroutineScope
30+
import kotlinx.coroutines.launch
2631

2732
interface AppReferrerDataStore {
2833
var referrerCheckedPreviously: Boolean
@@ -31,9 +36,27 @@ interface AppReferrerDataStore {
3136
var utmOriginAttributeCampaign: String?
3237
}
3338

34-
@ContributesBinding(AppScope::class)
39+
@ContributesBinding(
40+
scope = AppScope::class,
41+
boundType = AppReferrerDataStore::class,
42+
)
43+
@ContributesBinding(
44+
scope = AppScope::class,
45+
boundType = AppReferrer::class,
46+
)
3547
@SingleInstanceIn(AppScope::class)
36-
class AppReferenceSharePreferences @Inject constructor(private val context: Context) : AppReferrerDataStore {
48+
class AppReferenceSharePreferences @Inject constructor(
49+
private val context: Context,
50+
@AppCoroutineScope private val coroutineScope: CoroutineScope,
51+
private val dispatcherProvider: DispatcherProvider,
52+
) : AppReferrerDataStore, AppReferrer {
53+
54+
override fun setOriginAttributeCampaign(origin: String?) {
55+
coroutineScope.launch(dispatcherProvider.io()) {
56+
utmOriginAttributeCampaign = origin
57+
}
58+
}
59+
3760
override var campaignSuffix: String?
3861
get() = preferences.getString(KEY_CAMPAIGN_SUFFIX, null)
3962
set(value) = preferences.edit(true) { putString(KEY_CAMPAIGN_SUFFIX, value) }

app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import com.duckduckgo.app.pixels.AppPixelName
2121
import com.duckduckgo.app.referral.AppReferrerDataStore
2222
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
2323
import com.duckduckgo.app.statistics.pixels.Pixel
24-
import com.duckduckgo.app.statistics.store.StatisticsDataStore
2524
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2625
import com.duckduckgo.common.utils.DispatcherProvider
2726
import com.duckduckgo.di.scopes.AppScope
@@ -39,7 +38,6 @@ class AppReferrerInstallPixelSender @Inject constructor(
3938
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
4039
private val dispatchers: DispatcherProvider,
4140
private val appBuildConfig: AppBuildConfig,
42-
private val statisticsDataStore: StatisticsDataStore,
4341
) : AtbLifecyclePlugin {
4442

4543
private val pixelSent = AtomicBoolean(false)
@@ -57,8 +55,8 @@ class AppReferrerInstallPixelSender @Inject constructor(
5755
}
5856
}
5957

60-
private fun sendOriginAttribute(originAttribute: String?) {
61-
val returningUser = statisticsDataStore.variant == RETURNING_USER_VARIANT
58+
private suspend fun sendOriginAttribute(originAttribute: String?) {
59+
val returningUser = appBuildConfig.isAppReinstall()
6260

6361
val params = mutableMapOf(
6462
PIXEL_PARAM_LOCALE to appBuildConfig.deviceLocale.toLanguageTag(),
@@ -74,8 +72,6 @@ class AppReferrerInstallPixelSender @Inject constructor(
7472
}
7573

7674
companion object {
77-
private const val RETURNING_USER_VARIANT = "ru"
78-
7975
const val PIXEL_PARAM_ORIGIN = "origin"
8076
const val PIXEL_PARAM_LOCALE = "locale"
8177
const val PIXEL_PARAM_RETURNING_USER = "reinstall"

app/src/test/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParamPluginTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,28 @@
1616

1717
package com.duckduckgo.app.pixels.campaign.params
1818

19-
import com.duckduckgo.app.statistics.store.StatisticsDataStore
19+
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2020
import kotlinx.coroutines.test.runTest
2121
import org.junit.Assert
2222
import org.junit.Test
2323
import org.mockito.kotlin.mock
2424
import org.mockito.kotlin.whenever
2525

2626
class StatisticsAdditionalPixelParamPluginTest {
27+
private val appBuildConfig: AppBuildConfig = mock()
28+
2729
@Test
2830
fun whenRuVariantSetThenPluginShouldReturnParamTrue() = runTest {
29-
val statisticsDataStore: StatisticsDataStore = mock()
30-
whenever(statisticsDataStore.variant).thenReturn("ru")
31-
val plugin = ReinstallAdditionalPixelParamPlugin(statisticsDataStore)
31+
whenever(appBuildConfig.isAppReinstall()).thenReturn(true)
32+
val plugin = ReinstallAdditionalPixelParamPlugin(appBuildConfig)
3233

3334
Assert.assertEquals("isReinstall" to "true", plugin.params())
3435
}
3536

3637
@Test
3738
fun whenVariantIsNotRuThenPluginShouldReturnParamFalse() = runTest {
38-
val statisticsDataStore: StatisticsDataStore = mock()
39-
whenever(statisticsDataStore.variant).thenReturn("atb-1234")
40-
val plugin = ReinstallAdditionalPixelParamPlugin(statisticsDataStore)
39+
whenever(appBuildConfig.isAppReinstall()).thenReturn(false)
40+
val plugin = ReinstallAdditionalPixelParamPlugin(appBuildConfig)
4141

4242
Assert.assertEquals("isReinstall" to "false", plugin.params())
4343
}

app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.duckduckgo.app.pixels.AppPixelName.REFERRAL_INSTALL_UTM_CAMPAIGN
44
import com.duckduckgo.app.referral.AppReferrerDataStore
55
import com.duckduckgo.app.statistics.pixels.Pixel
66
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique
7-
import com.duckduckgo.app.statistics.store.StatisticsDataStore
87
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
98
import com.duckduckgo.common.test.CoroutineTestRule
109
import com.duckduckgo.referral.AppReferrerInstallPixelSender
@@ -35,14 +34,14 @@ class AppReferrerInstallPixelSenderTest {
3534
private val pixel: Pixel = mock()
3635
private val appBuildConfig: AppBuildConfig = mock()
3736
private val appReferrerDataStore: AppReferrerDataStore = mock()
38-
private val statisticsDataStore: StatisticsDataStore = mock()
3937
private val playStoreInstallChecker: VerificationCheckPlayStoreInstall = mock()
4038
private val captor = argumentCaptor<Map<String, String>>()
4139

4240
@Before
43-
fun setup() {
41+
fun setup() = runTest {
4442
whenever(appBuildConfig.deviceLocale).thenReturn(Locale.US)
4543
whenever(playStoreInstallChecker.installedFromPlayStore()).thenReturn(true)
44+
configureAsNewUser()
4645
}
4746

4847
private val testee = AppReferrerInstallPixelSender(
@@ -51,7 +50,6 @@ class AppReferrerInstallPixelSenderTest {
5150
appCoroutineScope = coroutineTestRule.testScope,
5251
dispatchers = coroutineTestRule.testDispatcherProvider,
5352
appBuildConfig = appBuildConfig,
54-
statisticsDataStore = statisticsDataStore,
5553
)
5654

5755
@Test
@@ -85,12 +83,12 @@ class AppReferrerInstallPixelSenderTest {
8583
verifyNoMoreInteractions(pixel)
8684
}
8785

88-
private fun configureAsReturningUser() {
89-
whenever(statisticsDataStore.variant).thenReturn("ru")
86+
private suspend fun configureAsReturningUser() {
87+
whenever(appBuildConfig.isAppReinstall()).thenReturn(true)
9088
}
9189

92-
private fun configureAsNewUser() {
93-
whenever(statisticsDataStore.variant).thenReturn("")
90+
private suspend fun configureAsNewUser() {
91+
whenever(appBuildConfig.isAppReinstall()).thenReturn(false)
9492
}
9593

9694
private fun configureReferrerCampaign(campaign: String?) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.browser.api.referrer
18+
19+
/** Public interface for app referral parameters */
20+
interface AppReferrer {
21+
22+
/**
23+
* Sets the attribute campaign origin.
24+
*/
25+
fun setOriginAttributeCampaign(origin: String?)
26+
}

experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/DownloadsDirectoryManager.kt

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)