Skip to content

Commit fca3fe3

Browse files
committed
Update metrics config from remote
1 parent f88ee1d commit fca3fe3

File tree

23 files changed

+1212
-344
lines changed

23 files changed

+1212
-344
lines changed

ad-click/ad-click-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ dependencies {
9898
testImplementation Testing.robolectric
9999

100100
testImplementation project(path: ':common-test')
101+
testImplementation project(path: ':feature-toggles-test')
101102

102103
coreLibraryDesugaring Android.tools.desugarJdkLibs
103104
}

ad-click/ad-click-impl/src/main/java/com/duckduckgo/adclick/impl/metrics/AdClickAttributedMetric.kt

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package com.duckduckgo.adclick.impl.metrics
1818

1919
import com.duckduckgo.app.attributed.metrics.api.AttributedMetric
2020
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricClient
21+
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricConfig
2122
import com.duckduckgo.app.attributed.metrics.api.EventStats
23+
import com.duckduckgo.app.attributed.metrics.api.MetricBucket
2224
import com.duckduckgo.app.di.AppCoroutineScope
2325
import com.duckduckgo.browser.api.install.AppInstall
2426
import com.duckduckgo.common.utils.DispatcherProvider
@@ -27,6 +29,9 @@ import com.squareup.anvil.annotations.ContributesBinding
2729
import com.squareup.anvil.annotations.ContributesMultibinding
2830
import dagger.SingleInstanceIn
2931
import kotlinx.coroutines.CoroutineScope
32+
import kotlinx.coroutines.CoroutineStart.LAZY
33+
import kotlinx.coroutines.Deferred
34+
import kotlinx.coroutines.async
3035
import kotlinx.coroutines.launch
3136
import logcat.logcat
3237
import java.time.Instant
@@ -54,26 +59,46 @@ class RealAdClickAttributedMetric @Inject constructor(
5459
private val dispatcherProvider: DispatcherProvider,
5560
private val attributedMetricClient: AttributedMetricClient,
5661
private val appInstall: AppInstall,
62+
private val attributedMetricConfig: AttributedMetricConfig,
5763
) : AttributedMetric, AdClickCollector {
5864

5965
companion object {
6066
private const val EVENT_NAME = "ad_click"
6167
private const val PIXEL_NAME = "user_average_ad_clicks_past_week"
68+
private const val FEATURE_TOGGLE_NAME = "adClickCountAvg"
69+
private const val FEATURE_EMIT_TOGGLE_NAME = "canEmitAdClickCountAvg"
6270
private const val DAYS_WINDOW = 7
63-
private val AD_CLICK_BUCKETS = arrayOf(2, 5)
6471
}
6572

66-
override fun onAdClick() {
67-
attributedMetricClient.collectEvent(EVENT_NAME)
73+
private val isEnabled: Deferred<Boolean> = appCoroutineScope.async(start = LAZY) {
74+
getToggle(FEATURE_TOGGLE_NAME)?.isEnabled() ?: false
75+
}
76+
77+
private val canEmit: Deferred<Boolean> = appCoroutineScope.async(start = LAZY) {
78+
getToggle(FEATURE_EMIT_TOGGLE_NAME)?.isEnabled() ?: false
79+
}
80+
81+
private val bucketConfig: Deferred<MetricBucket> = appCoroutineScope.async(start = LAZY) {
82+
attributedMetricConfig.getBucketConfiguration()[FEATURE_TOGGLE_NAME] ?: MetricBucket(
83+
buckets = listOf(2, 5),
84+
version = 0,
85+
)
86+
}
6887

88+
override fun onAdClick() {
6989
appCoroutineScope.launch(dispatcherProvider.io()) {
90+
if (!isEnabled.await()) return@launch
91+
attributedMetricClient.collectEvent(EVENT_NAME)
7092
if (shouldSendPixel().not()) {
7193
logcat(tag = "AttributedMetrics") {
7294
"AdClickCount7d: Skip emitting, not enough data or no events"
7395
}
7496
return@launch
7597
}
76-
attributedMetricClient.emitMetric(this@RealAdClickAttributedMetric)
98+
99+
if (canEmit.await()) {
100+
attributedMetricClient.emitMetric(this@RealAdClickAttributedMetric)
101+
}
77102
}
78103
}
79104

@@ -94,9 +119,10 @@ class RealAdClickAttributedMetric @Inject constructor(
94119
return daysSinceInstalled().toString()
95120
}
96121

97-
private fun getBucketValue(avg: Int): Int {
98-
return AD_CLICK_BUCKETS.indexOfFirst { bucket -> avg <= bucket }.let { index ->
99-
if (index == -1) AD_CLICK_BUCKETS.size else index
122+
private suspend fun getBucketValue(avg: Int): Int {
123+
val buckets = bucketConfig.await().buckets
124+
return buckets.indexOfFirst { bucket -> avg <= bucket }.let { index ->
125+
if (index == -1) buckets.size else index
100126
}
101127
}
102128

@@ -141,4 +167,9 @@ class RealAdClickAttributedMetric @Inject constructor(
141167

142168
return ChronoUnit.DAYS.between(installInEt.toLocalDate(), nowInEt.toLocalDate()).toInt()
143169
}
170+
171+
private suspend fun getToggle(toggleName: String) =
172+
attributedMetricConfig.metricsToggles().firstOrNull { toggle ->
173+
toggle.featureName().name == toggleName
174+
}
144175
}

ad-click/ad-click-impl/src/test/java/com/duckduckgo/adclick/impl/metrics/RealAdClickAttributedMetricTest.kt

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616

1717
package com.duckduckgo.adclick.impl.metrics
1818

19+
import android.annotation.SuppressLint
1920
import androidx.test.ext.junit.runners.AndroidJUnit4
2021
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricClient
22+
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricConfig
2123
import com.duckduckgo.app.attributed.metrics.api.EventStats
24+
import com.duckduckgo.app.attributed.metrics.api.MetricBucket
2225
import com.duckduckgo.browser.api.install.AppInstall
2326
import com.duckduckgo.common.test.CoroutineTestRule
27+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
28+
import com.duckduckgo.feature.toggles.api.Toggle
29+
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
30+
import com.duckduckgo.feature.toggles.api.Toggle.State
2431
import kotlinx.coroutines.test.runTest
2532
import org.junit.Assert.assertEquals
2633
import org.junit.Assert.assertNull
@@ -35,22 +42,39 @@ import org.mockito.kotlin.whenever
3542
import java.time.Instant
3643
import java.time.ZoneId
3744

45+
@SuppressLint("DenyListedApi")
3846
@RunWith(AndroidJUnit4::class)
3947
class RealAdClickAttributedMetricTest {
4048

4149
@get:Rule val coroutineRule = CoroutineTestRule()
4250

4351
private val attributedMetricClient: AttributedMetricClient = mock()
4452
private val appInstall: AppInstall = mock()
53+
private val attributedMetricConfig: AttributedMetricConfig = mock()
54+
private val adClickToggle = FakeFeatureToggleFactory.create(FakeAttributedMetricsConfigFeature::class.java)
4555

4656
private lateinit var testee: RealAdClickAttributedMetric
4757

48-
@Before fun setup() {
58+
@Before fun setup() = runTest {
59+
adClickToggle.adClickCountAvg().setRawStoredState(State(true))
60+
adClickToggle.canEmitAdClickCountAvg().setRawStoredState(State(true))
61+
whenever(attributedMetricConfig.metricsToggles()).thenReturn(
62+
listOf(adClickToggle.adClickCountAvg(), adClickToggle.canEmitAdClickCountAvg()),
63+
)
64+
whenever(attributedMetricConfig.getBucketConfiguration()).thenReturn(
65+
mapOf(
66+
"user_average_ad_clicks_past_week" to MetricBucket(
67+
buckets = listOf(2, 5),
68+
version = 0,
69+
),
70+
),
71+
)
4972
testee = RealAdClickAttributedMetric(
5073
appCoroutineScope = coroutineRule.testScope,
5174
dispatcherProvider = coroutineRule.testDispatcherProvider,
5275
attributedMetricClient = attributedMetricClient,
5376
appInstall = appInstall,
77+
attributedMetricConfig = attributedMetricConfig,
5478
)
5579
}
5680

@@ -99,6 +123,48 @@ class RealAdClickAttributedMetricTest {
99123
verify(attributedMetricClient).emitMetric(testee)
100124
}
101125

126+
@Test
127+
fun whenAdClickedButFFDisabledThenDoNotCollectAndDoNotEmitMetric() = runTest {
128+
adClickToggle.adClickCountAvg().setRawStoredState(State(false))
129+
whenever(attributedMetricConfig.metricsToggles()).thenReturn(
130+
listOf(adClickToggle.adClickCountAvg(), adClickToggle.canEmitAdClickCountAvg()),
131+
)
132+
givenDaysSinceInstalled(7)
133+
whenever(attributedMetricClient.getEventStats("ad_click", 7)).thenReturn(
134+
EventStats(
135+
daysWithEvents = 1,
136+
rollingAverage = 1.0,
137+
totalEvents = 1,
138+
),
139+
)
140+
141+
testee.onAdClick()
142+
143+
verify(attributedMetricClient, never()).collectEvent("ad_click")
144+
verify(attributedMetricClient, never()).emitMetric(testee)
145+
}
146+
147+
@Test fun whenAdClickedButEmitDisabledThenCollectButDoNotEmitMetric() = runTest {
148+
adClickToggle.adClickCountAvg().setRawStoredState(State(true))
149+
adClickToggle.canEmitAdClickCountAvg().setRawStoredState(State(false))
150+
whenever(attributedMetricConfig.metricsToggles()).thenReturn(
151+
listOf(adClickToggle.adClickCountAvg(), adClickToggle.canEmitAdClickCountAvg()),
152+
)
153+
givenDaysSinceInstalled(7)
154+
whenever(attributedMetricClient.getEventStats("ad_click", 7)).thenReturn(
155+
EventStats(
156+
daysWithEvents = 1,
157+
rollingAverage = 1.0,
158+
totalEvents = 1,
159+
),
160+
)
161+
162+
testee.onAdClick()
163+
164+
verify(attributedMetricClient).collectEvent("ad_click")
165+
verify(attributedMetricClient, never()).emitMetric(testee)
166+
}
167+
102168
@Test fun whenDaysInstalledLessThanWindowThenIncludeDayAverageParameter() = runTest {
103169
givenDaysSinceInstalled(5)
104170
whenever(attributedMetricClient.getEventStats("ad_click", 5)).thenReturn(
@@ -133,14 +199,17 @@ class RealAdClickAttributedMetricTest {
133199
// Map of average clicks to expected bucket value
134200
// clicks avg -> bucket
135201
val bucketRanges = mapOf(
136-
0.0 to 0,
137-
1.0 to 0,
138-
2.2 to 0,
139-
2.6 to 1,
140-
3.0 to 1,
141-
5.4 to 1,
142-
6.0 to 2,
143-
10.0 to 2,
202+
0.0 to 0, // 0 clicks -> bucket 0 (≤2)
203+
1.0 to 0, // 1 click -> bucket 0 (≤2)
204+
2.0 to 0, // 2 clicks -> bucket 0 (≤2)
205+
2.1 to 0, // 2.1 clicks rounds to 2 -> bucket 0 (≤2)
206+
2.5 to 1, // 2.5 clicks rounds to 3 -> bucket 1 (≤5)
207+
2.7 to 1, // 2.7 clicks rounds to 3 -> bucket 1 (≤5)
208+
3.0 to 1, // 3 clicks -> bucket 1 (≤5)
209+
5.0 to 1, // 5 clicks -> bucket 1 (≤5)
210+
5.1 to 1, // 5.1 clicks rounds to 5 -> bucket 1 (≤5)
211+
6.0 to 2, // 6 clicks -> bucket 2 (>5)
212+
10.0 to 2, // 10 clicks -> bucket 2 (>5)
144213
)
145214

146215
bucketRanges.forEach { (clicksAvg, expectedBucket) ->
@@ -194,3 +263,14 @@ class RealAdClickAttributedMetricTest {
194263
whenever(appInstall.getInstallationTimestamp()).thenReturn(installInEt.toInstant().toEpochMilli())
195264
}
196265
}
266+
267+
interface FakeAttributedMetricsConfigFeature {
268+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
269+
fun self(): Toggle
270+
271+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
272+
fun adClickCountAvg(): Toggle
273+
274+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
275+
fun canEmitAdClickCountAvg(): Toggle
276+
}

attributed-metrics/attributed-metrics-api/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ kotlin {
3333
dependencies {
3434
implementation Kotlin.stdlib.jdk7
3535
implementation KotlinX.coroutines.core
36+
37+
implementation project(path: ':feature-toggles-api')
3638
}

attributed-metrics/attributed-metrics-api/src/main/java/com/duckduckgo/app/attributed/metrics/api/AttributedMetricsClient.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.duckduckgo.app.attributed.metrics.api
1818

19+
import com.duckduckgo.feature.toggles.api.Toggle
20+
1921
/**
2022
* Client for collecting and emitting attributed metrics.
2123
*/
@@ -84,3 +86,22 @@ interface AttributedMetric {
8486
*/
8587
suspend fun getTag(): String
8688
}
89+
90+
interface AttributedMetricConfig {
91+
/**
92+
* Provides attributed Metrics subfeature Toggles. Each metric to find their toggle and react to enabled state.
93+
* @return List of Toggles that belong to Attributed Metrics feature
94+
*/
95+
suspend fun metricsToggles(): List<Toggle>
96+
97+
/**
98+
* Provides metrics bucket configuration, on a key-value. Each metric to consume map, and obtain the bucket config from he metric they own.
99+
* @return Map of metric keys to their bucket configurations
100+
*/
101+
suspend fun getBucketConfiguration(): Map<String, MetricBucket>
102+
}
103+
104+
data class MetricBucket(
105+
val buckets: List<Int>,
106+
val version: Int,
107+
)

attributed-metrics/attributed-metrics-impl/src/main/java/com/duckduckgo/app/attributed/metrics/AttributedMetricsConfigFeature.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,49 @@ import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
2828
interface AttributedMetricsConfigFeature {
2929
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
3030
fun self(): Toggle
31+
32+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
33+
fun emitAllMetrics(): Toggle
34+
35+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
36+
fun retention(): Toggle
37+
38+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
39+
fun canEmitRetention(): Toggle
40+
41+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
42+
fun searchDaysAvg(): Toggle
43+
44+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
45+
fun canEmitSearchDaysAvg(): Toggle
46+
47+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
48+
fun searchCountAvg(): Toggle
49+
50+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
51+
fun canEmitSearchCountAvg(): Toggle
52+
53+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
54+
fun adClickCountAvg(): Toggle
55+
56+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
57+
fun canEmitAdClickCountAvg(): Toggle
58+
59+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
60+
fun aiUsageAvg(): Toggle
61+
62+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
63+
fun canEmitAIUsageAvg(): Toggle
64+
65+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
66+
fun subscriptionRetention(): Toggle
67+
68+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
69+
fun canEmitSubscriptionRetention(): Toggle
70+
71+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
72+
fun syncDevices(): Toggle
73+
74+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
75+
fun canEmitSyncDevices(): Toggle
3176
}

0 commit comments

Comments
 (0)