Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}

Expand All @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -62,27 +63,57 @@ 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
fun whenReferrerContainsTargetWithDifferentCaseThenNoReferrerFound() {
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"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ class AtbInitializerTest {

private suspend fun referrerAnswer(delayMs: Long): Answer<ParsedReferrerResult> {
delay(delayMs)
return Answer { ParsedReferrerResult.ReferrerFound("") }
return Answer { ParsedReferrerResult.CampaignReferrerFound("") }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -131,6 +129,17 @@ class VariantManagerTest {
assertTrue(variant.hasFeature(ConceptTest))
}

@Test
fun verifyNoDuplicateVariantNames() {
val existingNames = mutableSetOf<String>()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/duckduckgo/app/global/AppUrl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ class AppUrl {

object ParamValue {
const val SOURCE = "ddg_android"
const val SOURCE_EU_AUCTION = "ddg_androideu"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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<String>): 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<String>): 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 {
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth adding an else to deal with unexpected referrers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did wonder about that, too, but decided not to since the else can mean a few different things (no referrer found, there was a problem parsing the referrer etc...) and I can't think of any single thing we'd want to do for each of them

}

appReferrerDataStore.referrerCheckedPreviously = true
}

Expand Down
Loading