Skip to content

Commit 85c065a

Browse files
committed
update metrics config from remote
1 parent f88ee1d commit 85c065a

File tree

23 files changed

+1208
-344
lines changed

23 files changed

+1208
-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: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ package com.duckduckgo.adclick.impl.metrics
1818

1919
import androidx.test.ext.junit.runners.AndroidJUnit4
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.browser.api.install.AppInstall
2325
import com.duckduckgo.common.test.CoroutineTestRule
26+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
27+
import com.duckduckgo.feature.toggles.api.Toggle
28+
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
29+
import com.duckduckgo.feature.toggles.api.Toggle.State
2430
import kotlinx.coroutines.test.runTest
2531
import org.junit.Assert.assertEquals
2632
import org.junit.Assert.assertNull
@@ -42,15 +48,31 @@ class RealAdClickAttributedMetricTest {
4248

4349
private val attributedMetricClient: AttributedMetricClient = mock()
4450
private val appInstall: AppInstall = mock()
51+
private val attributedMetricConfig: AttributedMetricConfig = mock()
52+
private val adClickToggle = FakeFeatureToggleFactory.create(FakeAttributedMetricsConfigFeature::class.java)
4553

4654
private lateinit var testee: RealAdClickAttributedMetric
4755

48-
@Before fun setup() {
56+
@Before fun setup() = runTest {
57+
adClickToggle.adClickCountAvg().setRawStoredState(State(true))
58+
adClickToggle.canEmitAdClickCountAvg().setRawStoredState(State(true))
59+
whenever(attributedMetricConfig.metricsToggles()).thenReturn(
60+
listOf(adClickToggle.adClickCountAvg(), adClickToggle.canEmitAdClickCountAvg()),
61+
)
62+
whenever(attributedMetricConfig.getBucketConfiguration()).thenReturn(
63+
mapOf(
64+
"user_average_ad_clicks_past_week" to MetricBucket(
65+
buckets = listOf(2, 5),
66+
version = 0,
67+
),
68+
),
69+
)
4970
testee = RealAdClickAttributedMetric(
5071
appCoroutineScope = coroutineRule.testScope,
5172
dispatcherProvider = coroutineRule.testDispatcherProvider,
5273
attributedMetricClient = attributedMetricClient,
5374
appInstall = appInstall,
75+
attributedMetricConfig = attributedMetricConfig,
5476
)
5577
}
5678

@@ -99,6 +121,48 @@ class RealAdClickAttributedMetricTest {
99121
verify(attributedMetricClient).emitMetric(testee)
100122
}
101123

124+
@Test
125+
fun whenAdClickedButFFDisabledThenDoNotCollectAndDoNotEmitMetric() = runTest {
126+
adClickToggle.adClickCountAvg().setRawStoredState(State(false))
127+
whenever(attributedMetricConfig.metricsToggles()).thenReturn(
128+
listOf(adClickToggle.adClickCountAvg(), adClickToggle.canEmitAdClickCountAvg()),
129+
)
130+
givenDaysSinceInstalled(7)
131+
whenever(attributedMetricClient.getEventStats("ad_click", 7)).thenReturn(
132+
EventStats(
133+
daysWithEvents = 1,
134+
rollingAverage = 1.0,
135+
totalEvents = 1,
136+
),
137+
)
138+
139+
testee.onAdClick()
140+
141+
verify(attributedMetricClient, never()).collectEvent("ad_click")
142+
verify(attributedMetricClient, never()).emitMetric(testee)
143+
}
144+
145+
@Test fun whenAdClickedButEmitDisabledThenCollectButDoNotEmitMetric() = runTest {
146+
adClickToggle.adClickCountAvg().setRawStoredState(State(true))
147+
adClickToggle.canEmitAdClickCountAvg().setRawStoredState(State(false))
148+
whenever(attributedMetricConfig.metricsToggles()).thenReturn(
149+
listOf(adClickToggle.adClickCountAvg(), adClickToggle.canEmitAdClickCountAvg()),
150+
)
151+
givenDaysSinceInstalled(7)
152+
whenever(attributedMetricClient.getEventStats("ad_click", 7)).thenReturn(
153+
EventStats(
154+
daysWithEvents = 1,
155+
rollingAverage = 1.0,
156+
totalEvents = 1,
157+
),
158+
)
159+
160+
testee.onAdClick()
161+
162+
verify(attributedMetricClient).collectEvent("ad_click")
163+
verify(attributedMetricClient, never()).emitMetric(testee)
164+
}
165+
102166
@Test fun whenDaysInstalledLessThanWindowThenIncludeDayAverageParameter() = runTest {
103167
givenDaysSinceInstalled(5)
104168
whenever(attributedMetricClient.getEventStats("ad_click", 5)).thenReturn(
@@ -133,14 +197,17 @@ class RealAdClickAttributedMetricTest {
133197
// Map of average clicks to expected bucket value
134198
// clicks avg -> bucket
135199
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,
200+
0.0 to 0, // 0 clicks -> bucket 0 (≤2)
201+
1.0 to 0, // 1 click -> bucket 0 (≤2)
202+
2.0 to 0, // 2 clicks -> bucket 0 (≤2)
203+
2.1 to 0, // 2.1 clicks rounds to 2 -> bucket 0 (≤2)
204+
2.5 to 1, // 2.5 clicks rounds to 3 -> bucket 1 (≤5)
205+
2.7 to 1, // 2.7 clicks rounds to 3 -> bucket 1 (≤5)
206+
3.0 to 1, // 3 clicks -> bucket 1 (≤5)
207+
5.0 to 1, // 5 clicks -> bucket 1 (≤5)
208+
5.1 to 1, // 5.1 clicks rounds to 5 -> bucket 1 (≤5)
209+
6.0 to 2, // 6 clicks -> bucket 2 (>5)
210+
10.0 to 2, // 10 clicks -> bucket 2 (>5)
144211
)
145212

146213
bucketRanges.forEach { (clicksAvg, expectedBucket) ->
@@ -194,3 +261,14 @@ class RealAdClickAttributedMetricTest {
194261
whenever(appInstall.getInstallationTimestamp()).thenReturn(installInEt.toInstant().toEpochMilli())
195262
}
196263
}
264+
265+
interface FakeAttributedMetricsConfigFeature {
266+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
267+
fun self(): Toggle
268+
269+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
270+
fun adClickCountAvg(): Toggle
271+
272+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
273+
fun canEmitAdClickCountAvg(): Toggle
274+
}

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)