diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriterTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriterTest.kt index 7c7ca7a28427..d7cea57645f1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriterTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriterTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser import android.net.Uri import com.duckduckgo.app.global.AppUrl.ParamKey +import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.model.Atb import com.duckduckgo.app.statistics.store.StatisticsDataStore @@ -30,14 +31,16 @@ import org.junit.Test class DuckDuckGoRequestRewriterTest { private lateinit var testee: DuckDuckGoRequestRewriter - private var mockStatisticsStore: StatisticsDataStore = mock() - private var mockVariantManager: VariantManager = mock() + private val mockStatisticsStore: StatisticsDataStore = mock() + private val mockVariantManager: VariantManager = mock() + private val mockAppReferrerDataStore: AppReferrerDataStore = mock() private lateinit var builder: Uri.Builder @Before fun before() { whenever(mockVariantManager.getVariant()).thenReturn(VariantManager.DEFAULT_VARIANT) - testee = DuckDuckGoRequestRewriter(DuckDuckGoUrlDetector(), mockStatisticsStore, mockVariantManager) + whenever(mockAppReferrerDataStore.installedFromEuAuction).thenReturn(false) + testee = DuckDuckGoRequestRewriter(DuckDuckGoUrlDetector(), mockStatisticsStore, mockVariantManager, mockAppReferrerDataStore) builder = Uri.Builder() } @@ -49,6 +52,15 @@ class DuckDuckGoRequestRewriterTest { assertEquals("ddg_android", uri.getQueryParameter(ParamKey.SOURCE)) } + @Test + fun whenAddingCustomParamsAndUserSourcedFromEuAuctionThenEuSourceParameterIsAdded() { + whenever(mockAppReferrerDataStore.installedFromEuAuction).thenReturn(true) + testee.addCustomQueryParams(builder) + val uri = builder.build() + assertTrue(uri.queryParameterNames.contains(ParamKey.SOURCE)) + assertEquals("ddg_androideu", uri.getQueryParameter(ParamKey.SOURCE)) + } + @Test fun whenAddingCustomParamsIfStoreContainsAtbIsAdded() { whenever(mockStatisticsStore.atb).thenReturn(Atb("v105-2ma")) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/QueryUrlConverterTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/QueryUrlConverterTest.kt index 55281fa17818..9c8e4ef49da4 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/QueryUrlConverterTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/QueryUrlConverterTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser import android.net.Uri import com.duckduckgo.app.browser.omnibar.QueryUrlConverter +import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.nhaarman.mockitokotlin2.mock @@ -28,7 +29,8 @@ class QueryUrlConverterTest { private var mockStatisticsStore: StatisticsDataStore = mock() private val variantManager: VariantManager = mock() - private val requestRewriter = DuckDuckGoRequestRewriter(DuckDuckGoUrlDetector(), mockStatisticsStore, variantManager) + private val mockAppReferrerDataStore: AppReferrerDataStore = mock() + private val requestRewriter = DuckDuckGoRequestRewriter(DuckDuckGoUrlDetector(), mockStatisticsStore, variantManager, mockAppReferrerDataStore) private val testee: QueryUrlConverter = QueryUrlConverter(requestRewriter) @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt index fc8de86c32d9..92b38cc5a1c0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt @@ -120,7 +120,7 @@ class LaunchViewModelTest { override suspend fun waitForReferrerCode(): ParsedReferrerResult { if (mockDelayMs > 0) delay(mockDelayMs) - return ParsedReferrerResult.ReferrerFound(referrer) + return ParsedReferrerResult.CampaignReferrerFound(referrer) } override fun initialiseReferralRetrieval() { diff --git a/app/src/androidTest/java/com/duckduckgo/app/referral/QueryParamReferrerParserTest.kt b/app/src/androidTest/java/com/duckduckgo/app/referral/QueryParamReferrerParserTest.kt index 4d6f8d46dd9f..296e5243eeb6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/referral/QueryParamReferrerParserTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/referral/QueryParamReferrerParserTest.kt @@ -16,7 +16,8 @@ package com.duckduckgo.app.referral -import com.duckduckgo.app.referral.ParsedReferrerResult.ReferrerFound +import com.duckduckgo.app.referral.ParsedReferrerResult.CampaignReferrerFound +import com.duckduckgo.app.referral.ParsedReferrerResult.EuAuctionReferrerFound import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -33,13 +34,13 @@ class QueryParamReferrerParserTest { @Test fun whenReferrerContainsTargetAndLongSuffixThenShortenedReferrerFound() { val result = testee.parse("DDGRAABC") - verifyReferrerFound("AB", result) + verifyCampaignReferrerFound("AB", result) } @Test fun whenReferrerContainsTargetAndTwoCharSuffixThenReferrerFound() { val result = testee.parse("DDGRAXY") - verifyReferrerFound("XY", result) + verifyCampaignReferrerFound("XY", result) } @Test @@ -62,13 +63,13 @@ class QueryParamReferrerParserTest { @Test fun whenReferrerContainsTargetAsFirstParamThenReferrerFound() { val result = testee.parse("key1=DDGRAAB&key2=foo&key3=bar") - verifyReferrerFound("AB", result) + verifyCampaignReferrerFound("AB", result) } @Test fun whenReferrerContainsTargetAsLastParamThenReferrerFound() { val result = testee.parse("key1=foo&key2=bar&key3=DDGRAAB") - verifyReferrerFound("AB", result) + verifyCampaignReferrerFound("AB", result) } @Test @@ -76,13 +77,43 @@ class QueryParamReferrerParserTest { verifyReferrerNotFound(testee.parse("ddgraAB")) } - private fun verifyReferrerFound(expectedReferrer: String, result: ParsedReferrerResult) { - assertTrue(result is ReferrerFound) - val value = (result as ReferrerFound).campaignSuffix + @Test + fun whenReferrerContainsEuAuctionDataThenEuActionReferrerFound() { + val result = testee.parse("$INSTALLATION_SOURCE_KEY=$INSTALLATION_SOURCE_EU_AUCTION_VALUE") + assertTrue(result is EuAuctionReferrerFound) + } + + @Test + fun whenReferrerContainsBothEuAuctionAndCampaignReferrerDataThenEuActionReferrerFound() { + val result = testee.parse("key1=DDGRAAB&key2=foo&key3=bar&$INSTALLATION_SOURCE_KEY=$INSTALLATION_SOURCE_EU_AUCTION_VALUE") + assertTrue(result is EuAuctionReferrerFound) + } + + @Test + fun whenReferrerContainsInstallationSourceKeyButNotMatchingValueThenNoReferrerFound() { + val result = testee.parse("$INSTALLATION_SOURCE_KEY=bar") + verifyReferrerNotFound(result) + } + + @Test + fun whenReferrerContainsInstallationSourceKeyAndNoEuAuctionValueButHasCampaignReferrerDataThenCampaignReferrerFound() { + val result = testee.parse("key1=DDGRAAB&key2=foo&key3=bar&$INSTALLATION_SOURCE_KEY=bar") + verifyCampaignReferrerFound("AB", result) + } + + private fun verifyCampaignReferrerFound(expectedReferrer: String, result: ParsedReferrerResult) { + assertTrue(result is CampaignReferrerFound) + val value = (result as CampaignReferrerFound).campaignSuffix assertEquals(expectedReferrer, value) } private fun verifyReferrerNotFound(result: ParsedReferrerResult) { assertTrue(result is ParsedReferrerResult.ReferrerNotFound) } + + companion object { + private const val INSTALLATION_SOURCE_KEY = "utm_source" + private const val INSTALLATION_SOURCE_EU_AUCTION_VALUE = "eea-search-choice" + } + } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt index ecb4846c9894..486b93409346 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt @@ -88,6 +88,6 @@ class AtbInitializerTest { private suspend fun referrerAnswer(delayMs: Long): Answer { delay(delayMs) - return Answer { ParsedReferrerResult.ReferrerFound("") } + return Answer { ParsedReferrerResult.CampaignReferrerFound("") } } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt index 9b0a27affd69..416531f357d4 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt @@ -17,9 +17,7 @@ package com.duckduckgo.app.statistics import com.duckduckgo.app.statistics.VariantManager.VariantFeature.* -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assert.fail +import org.junit.Assert.* import org.junit.Test class VariantManagerTest { @@ -131,6 +129,17 @@ class VariantManagerTest { assertTrue(variant.hasFeature(ConceptTest)) } + @Test + fun verifyNoDuplicateVariantNames() { + val existingNames = mutableSetOf() + variants.forEach { + if (!existingNames.add(it.key)) { + fail("Duplicate variant name found: ${it.key}") + } + } + } + + @Suppress("SameParameterValue") private fun assertEqualsDouble(expected: Double, actual: Double) { val comparison = expected.compareTo(actual) diff --git a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriter.kt b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriter.kt index 67412ce14d22..49f5bfa111e7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriter.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoRequestRewriter.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.browser import android.net.Uri import com.duckduckgo.app.global.AppUrl.ParamKey import com.duckduckgo.app.global.AppUrl.ParamValue +import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.store.StatisticsDataStore import timber.log.Timber @@ -32,7 +33,8 @@ interface RequestRewriter { class DuckDuckGoRequestRewriter( private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, private val statisticsStore: StatisticsDataStore, - private val variantManager: VariantManager + private val variantManager: VariantManager, + private val appReferrerDataStore: AppReferrerDataStore ) : RequestRewriter { override fun rewriteRequestWithCustomQueryParams(request: Uri): Uri { @@ -67,6 +69,8 @@ class DuckDuckGoRequestRewriter( if (atb != null) { builder.appendQueryParameter(ParamKey.ATB, atb.formatWithVariant(variantManager.getVariant())) } - builder.appendQueryParameter(ParamKey.SOURCE, ParamValue.SOURCE) + + val sourceValue = if (appReferrerDataStore.installedFromEuAuction) ParamValue.SOURCE_EU_AUCTION else ParamValue.SOURCE + builder.appendQueryParameter(ParamKey.SOURCE, sourceValue) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 617dc9659f14..fdce382e76cd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -39,6 +39,7 @@ import com.duckduckgo.app.global.file.FileDeleter import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.httpsupgrade.HttpsUpgrader import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao +import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore @@ -57,9 +58,10 @@ class BrowserModule { fun duckDuckGoRequestRewriter( urlDetector: DuckDuckGoUrlDetector, statisticsStore: StatisticsDataStore, - variantManager: VariantManager + variantManager: VariantManager, + appReferrerDataStore: AppReferrerDataStore ): RequestRewriter { - return DuckDuckGoRequestRewriter(urlDetector, statisticsStore, variantManager) + return DuckDuckGoRequestRewriter(urlDetector, statisticsStore, variantManager, appReferrerDataStore) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/global/AppUrl.kt b/app/src/main/java/com/duckduckgo/app/global/AppUrl.kt index 12f5380f8646..bf41bebba264 100644 --- a/app/src/main/java/com/duckduckgo/app/global/AppUrl.kt +++ b/app/src/main/java/com/duckduckgo/app/global/AppUrl.kt @@ -40,5 +40,6 @@ class AppUrl { object ParamValue { const val SOURCE = "ddg_android" + const val SOURCE_EU_AUCTION = "ddg_androideu" } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerParser.kt b/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerParser.kt index 36562f2ff02d..ecf715422d88 100644 --- a/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerParser.kt +++ b/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerParser.kt @@ -16,8 +16,7 @@ package com.duckduckgo.app.referral -import com.duckduckgo.app.referral.ParsedReferrerResult.ReferrerFound -import com.duckduckgo.app.referral.ParsedReferrerResult.ReferrerNotFound +import com.duckduckgo.app.referral.ParsedReferrerResult.* import timber.log.Timber @@ -33,16 +32,41 @@ class QueryParamReferrerParser : AppInstallationReferrerParser { val referrerParts = splitIntoConstituentParts(referrer) if (referrerParts.isNullOrEmpty()) return ReferrerNotFound(fromCache = false) + val auctionReferrer = extractEuAuctionReferrer(referrerParts) + if (auctionReferrer is EuAuctionReferrerFound) { + return auctionReferrer + } + + return extractCampaignReferrer(referrerParts) + } + + private fun extractEuAuctionReferrer(referrerParts: List): ParsedReferrerResult { + Timber.d("Looking for Google EU Auction referrer data") + for (part in referrerParts) { + + Timber.v("Analysing query param part: $part") + if (part.startsWith(INSTALLATION_SOURCE_KEY) && part.endsWith(INSTALLATION_SOURCE_EU_AUCTION_VALUE)) { + Timber.i("App installed as a result of the EU auction") + return EuAuctionReferrerFound() + } + } + + Timber.d("App not installed as a result of EU auction") + return ReferrerNotFound() + } + + private fun extractCampaignReferrer(referrerParts: List): ParsedReferrerResult { + Timber.d("Looking for regular referrer data") for (part in referrerParts) { - Timber.d("Analysing query param part: $part") + Timber.v("Analysing query param part: $part") if (part.contains(CAMPAIGN_NAME_PREFIX)) { return extractCampaignNameSuffix(part, CAMPAIGN_NAME_PREFIX) } } - Timber.i("Referrer information does not contain inspected campaign names") - return ReferrerNotFound(fromCache = false) + Timber.d("Referrer information does not contain inspected campaign names") + return ReferrerNotFound() } private fun extractCampaignNameSuffix(part: String, prefix: String): ParsedReferrerResult { @@ -56,7 +80,7 @@ class QueryParamReferrerParser : AppInstallationReferrerParser { val condensedSuffix = suffix.take(2) Timber.i("Found suffix $condensedSuffix (looking for ${prefix}, found in $part)") - return ReferrerFound(condensedSuffix) + return CampaignReferrerFound(condensedSuffix) } private fun stripCampaignName(fullCampaignName: String, prefix: String): String { @@ -69,11 +93,15 @@ class QueryParamReferrerParser : AppInstallationReferrerParser { companion object { private const val CAMPAIGN_NAME_PREFIX = "DDGRA" + + private const val INSTALLATION_SOURCE_KEY = "utm_source" + private const val INSTALLATION_SOURCE_EU_AUCTION_VALUE = "eea-search-choice" } } sealed class ParsedReferrerResult(open val fromCache: Boolean = false) { - data class ReferrerFound(val campaignSuffix: String, override val fromCache: Boolean = false) : ParsedReferrerResult(fromCache) + data class EuAuctionReferrerFound(override val fromCache: Boolean = false) : ParsedReferrerResult(fromCache) + data class CampaignReferrerFound(val campaignSuffix: String, override val fromCache: Boolean = false) : ParsedReferrerResult(fromCache) data class ReferrerNotFound(override val fromCache: Boolean = false) : ParsedReferrerResult(fromCache) data class ParseFailure(val reason: ParseFailureReason) : ParsedReferrerResult() object ReferrerInitialising : ParsedReferrerResult() diff --git a/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerStateListener.kt b/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerStateListener.kt index 13d421bafe29..d3106dfe4778 100644 --- a/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerStateListener.kt +++ b/app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerStateListener.kt @@ -64,8 +64,14 @@ class PlayStoreAppReferrerStateListener @Inject constructor( initialisationStartTime = System.currentTimeMillis() if (appReferrerDataStore.referrerCheckedPreviously) { - referralResult = loadPreviousReferrerData() - Timber.i("Already inspected this referrer data. Took ${System.currentTimeMillis() - initialisationStartTime}ms to load from disk") + + referralResult = if (appReferrerDataStore.installedFromEuAuction) { + EuAuctionReferrerFound(fromCache = true) + } else { + loadPreviousReferrerData() + } + + Timber.i("Already inspected this referrer data") return } @@ -87,7 +93,7 @@ class PlayStoreAppReferrerStateListener @Inject constructor( ReferrerNotFound(fromCache = true) } else { Timber.i("Already have referrer data from previous run - $suffix") - ReferrerFound(suffix, fromCache = true) + CampaignReferrerFound(suffix, fromCache = true) } } @@ -155,11 +161,17 @@ class PlayStoreAppReferrerStateListener @Inject constructor( private fun referralResultReceived(result: ParsedReferrerResult) { referralResult = result - if (result is ReferrerFound) { - variantManager.updateAppReferrerVariant(result.campaignSuffix) - appReferrerDataStore.campaignSuffix = result.campaignSuffix - + when (result) { + is CampaignReferrerFound -> { + variantManager.updateAppReferrerVariant(result.campaignSuffix) + appReferrerDataStore.campaignSuffix = result.campaignSuffix + } + is EuAuctionReferrerFound -> { + variantManager.updateAppReferrerVariant(VariantManager.RESERVED_EU_AUCTION_VARIANT) + appReferrerDataStore.installedFromEuAuction = true + } } + appReferrerDataStore.referrerCheckedPreviously = true } diff --git a/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt b/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt index d8019b3ee326..6c53a3584a4a 100644 --- a/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt @@ -23,6 +23,7 @@ import androidx.core.content.edit interface AppReferrerDataStore { var referrerCheckedPreviously: Boolean var campaignSuffix: String? + var installedFromEuAuction: Boolean } class AppReferenceSharePreferences(private val context: Context) : AppReferrerDataStore { @@ -34,6 +35,10 @@ class AppReferenceSharePreferences(private val context: Context) : AppReferrerDa get() = preferences.getBoolean(KEY_CHECKED_PREVIOUSLY, false) set(value) = preferences.edit(true) { putBoolean(KEY_CHECKED_PREVIOUSLY, value) } + override var installedFromEuAuction: Boolean + get() = preferences.getBoolean(KEY_INSTALLED_FROM_EU_AUCTION, false) + set(value) = preferences.edit(true) { putBoolean(KEY_INSTALLED_FROM_EU_AUCTION, value) } + private val preferences: SharedPreferences get() = context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) @@ -41,5 +46,6 @@ class AppReferenceSharePreferences(private val context: Context) : AppReferrerDa const val FILENAME = "com.duckduckgo.app.referral" private const val KEY_CAMPAIGN_SUFFIX = "KEY_CAMPAIGN_SUFFIX" private const val KEY_CHECKED_PREVIOUSLY = "KEY_CHECKED_PREVIOUSLY" + private const val KEY_INSTALLED_FROM_EU_AUCTION = "KEY_INSTALLED_FROM_EU_AUCTION" } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt index 826c852c2148..91f3b6fa9c96 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt @@ -37,6 +37,8 @@ interface VariantManager { companion object { + const val RESERVED_EU_AUCTION_VARIANT = "ml" + // this will be returned when there are no other active experiments val DEFAULT_VARIANT = Variant(key = "", features = emptyList(), filterBy = { noFilter() })