Skip to content

Commit e0c4a87

Browse files
aitorvskarlenDimla
authored andcommitted
Support experiments in Active plugins (#4860)
Task/Issue URL: https://app.asana.com/0/488551667048375/1207977093713099/f ### Description Add support for experiments in active plugins. Add support to force active plugins enabled for internal builds. ### Steps to test this PR Ensure that DuDuckGo folder has been removed from Downloads folder to avoid returning user. _Control Group - mq - Play Flavour_ - [ ] Change Privacy Config URL to `https://jsonblob.com/api/jsonBlob/1243573265112096768` - [ ] Fresh install - [ ] Filter log to validate that `mq` variant has been assigned - [ ] Complete Onboarding and open New Tab - [ ] Verify legacy NTP is visible - [ ] Try a few times, ensure only legacy version is available _Variant Group - mr - Play Flavour_ - [ ] Change Privacy Config URL to `https://jsonblob.com/api/jsonBlob/1270091613312245760` - [ ] Fresh install - [ ] Filter log to validate that `mr` variant has been assigned - [ ] Complete Onboarding and open New Tab - [ ] Verify new NTP is visible - [ ] Try a few times, ensure only new version is available _Control Group - mq - Internal Flavour_ - [x] Change Privacy Config URL to `https://jsonblob.com/api/jsonBlob/1243573265112096768 ` - [x] Fresh install - [x] Filter log to validate that `mr` variant has been assigned - [x] Complete Onboarding and open New Tab - [x] Verify new NTP is visible - [x] Try a few times, ensure only new version is available
1 parent 3a7d592 commit e0c4a87

File tree

6 files changed

+159
-4
lines changed

6 files changed

+159
-4
lines changed

anvil/anvil-annotations/src/main/java/com/duckduckgo/anvil/annotations/ContributesActivePlugin.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,16 @@ annotation class ContributesActivePlugin(
6060
* The [ContributesActivePlugin] coalesce both
6161
*/
6262
val priority: Int = 0,
63+
64+
/**
65+
* When `true` the backing feature flag supports experimentation. Otherwise it will be a regular feature flag.
66+
*
67+
* When `true` the (generated) backing feature flag is annotated with the [Experiment] annotation (read its JavaDoc)
68+
*/
69+
val supportExperiments: Boolean = false,
70+
71+
/**
72+
* When `true` the backing feature flag will ALWAYS be enabled for internal builds, regardless of remote config or default value.
73+
*/
74+
val internalAlwaysEnabled: Boolean = false,
6375
)

anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesActivePluginPointCodeGenerator.kt

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
2323
import com.duckduckgo.anvil.annotations.PriorityKey
2424
import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed
2525
import com.duckduckgo.feature.toggles.api.Toggle
26+
import com.duckduckgo.feature.toggles.api.Toggle.Experiment
27+
import com.duckduckgo.feature.toggles.api.Toggle.InternalAlwaysEnabled
2628
import com.google.auto.service.AutoService
2729
import com.squareup.anvil.annotations.ContributesBinding
2830
import com.squareup.anvil.annotations.ContributesMultibinding
@@ -314,6 +316,12 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator {
314316
val pluginRemoteFeatureClassName = "${vmClass.shortName}_ActivePlugin_RemoteFeature"
315317
val pluginRemoteFeatureStoreClassName = "${vmClass.shortName}_ActivePlugin_RemoteFeature_MultiProcessStore"
316318
val pluginPriority = vmClass.annotations.firstOrNull { it.fqName == ContributesActivePlugin::class.fqName }?.priorityOrNull()
319+
val pluginSupportExperiments = vmClass.annotations.firstOrNull {
320+
it.fqName == ContributesActivePlugin::class.fqName
321+
}?.isExperimentOrNull() ?: false
322+
val pluginInternalAlwaysEnabled = vmClass.annotations.firstOrNull {
323+
it.fqName == ContributesActivePlugin::class.fqName
324+
}?.internalAlwaysEnabledOrNull() ?: false
317325

318326
// Check if there's another plugin class, in the same plugin point, that has the same class simplename
319327
// we can't allow that because the backing remote feature would be the same
@@ -403,14 +411,24 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator {
403411
.build(),
404412
)
405413
addFunction(
406-
FunSpec.builder(featureName)
407-
.addModifiers(ABSTRACT)
408-
.addAnnotation(
414+
FunSpec.builder(featureName).apply {
415+
addModifiers(ABSTRACT)
416+
addAnnotation(
409417
AnnotationSpec.builder(Toggle.DefaultValue::class)
410418
.addMember("defaultValue = %L", featureDefaultValue)
411419
.build(),
412420
)
413-
.returns(Toggle::class)
421+
// If the active plugin defines [supportExperiments = true] we mark it as Experiment
422+
if (pluginSupportExperiments) {
423+
addAnnotation(AnnotationSpec.builder(Experiment::class).build())
424+
}
425+
// If the active plugin defines [internalAlwaysEnabled = true] we mark it as InternalAlwaysEnabled
426+
if (pluginInternalAlwaysEnabled) {
427+
addAnnotation(AnnotationSpec.builder(InternalAlwaysEnabled::class).build())
428+
}
429+
430+
returns(Toggle::class)
431+
}
414432
.build(),
415433
)
416434
}.build(),
@@ -579,6 +597,12 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator {
579597
@OptIn(ExperimentalAnvilApi::class)
580598
private fun AnnotationReference.priorityOrNull(): Int? = argumentAt("priority", 3)?.value()
581599

600+
@OptIn(ExperimentalAnvilApi::class)
601+
private fun AnnotationReference.isExperimentOrNull(): Boolean? = argumentAt("supportExperiments", 4)?.value()
602+
603+
@OptIn(ExperimentalAnvilApi::class)
604+
private fun AnnotationReference.internalAlwaysEnabledOrNull(): Boolean? = argumentAt("internalAlwaysEnabled", 5)?.value()
605+
582606
private fun ClassReference.Psi.pluginClassName(
583607
fqName: FqName,
584608
): ClassName? {

app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class RealFocusedViewProvider @Inject constructor(
5050
scope = ActivityScope::class,
5151
boundType = FocusedViewPlugin::class,
5252
priority = 100,
53+
supportExperiments = true,
5354
)
5455
class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin {
5556

@@ -65,6 +66,8 @@ class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin {
6566
boundType = FocusedViewPlugin::class,
6667
priority = 0,
6768
defaultActiveValue = false,
69+
supportExperiments = true,
70+
internalAlwaysEnabled = true,
6871
)
6972
class FocusedPage @Inject constructor() : FocusedViewPlugin {
7073

feature-toggles/feature-toggles-impl/src/test/java/com/duckduckgo/feature/toggles/codegen/ContributesActivePluginPointCodeGeneratorTest.kt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ import com.duckduckgo.data.store.api.SharedPreferencesProvider
2626
import com.duckduckgo.di.scopes.AppScope
2727
import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed
2828
import com.duckduckgo.feature.toggles.api.Toggle
29+
import com.duckduckgo.feature.toggles.api.Toggle.Experiment
30+
import com.duckduckgo.feature.toggles.api.Toggle.InternalAlwaysEnabled
2931
import com.squareup.moshi.Moshi
3032
import kotlin.reflect.KClass
3133
import kotlin.reflect.full.functions
3234
import kotlinx.coroutines.CoroutineScope
3335
import org.junit.Assert.assertEquals
3436
import org.junit.Assert.assertFalse
3537
import org.junit.Assert.assertNotNull
38+
import org.junit.Assert.assertNull
3639
import org.junit.Assert.assertTrue
3740
import org.junit.Rule
3841
import org.junit.Test
@@ -114,6 +117,94 @@ class ContributesActivePluginPointCodeGeneratorTest {
114117
assertEquals(expectedClass.kotlin, featureAnnotation.toggleStore)
115118
}
116119

120+
@Test
121+
fun `test generated experiment remote features`() {
122+
val clazz = Class
123+
.forName("com.duckduckgo.feature.toggles.codegen.ExperimentActivePlugin_ActivePlugin_RemoteFeature")
124+
125+
assertNotNull(clazz.methods.find { it.name == "self" && it.returnType.kotlin == Toggle::class })
126+
assertNotNull(clazz.methods.find { it.name == "pluginExperimentActivePlugin" && it.returnType.kotlin == Toggle::class })
127+
128+
assertNotNull(
129+
clazz.kotlin.functions.firstOrNull { it.name == "self" }!!.annotations.firstOrNull { it.annotationClass == Toggle.DefaultValue::class },
130+
)
131+
assertNull(
132+
clazz.kotlin.functions.firstOrNull { it.name == "self" }!!.annotations.firstOrNull { it.annotationClass == Toggle.Experiment::class },
133+
)
134+
assertNull(
135+
clazz.kotlin.functions.firstOrNull { it.name == "self" }!!
136+
.annotations.firstOrNull { it.annotationClass == Toggle.InternalAlwaysEnabled::class },
137+
)
138+
assertNotNull(
139+
clazz.kotlin.functions.firstOrNull { it.name == "pluginExperimentActivePlugin" }!!.annotations
140+
.firstOrNull { it.annotationClass == Toggle.DefaultValue::class },
141+
)
142+
assertNotNull(
143+
clazz.kotlin.functions.firstOrNull { it.name == "pluginExperimentActivePlugin" }!!.annotations
144+
.firstOrNull { it.annotationClass == Toggle.Experiment::class },
145+
)
146+
assertNull(
147+
clazz.kotlin.functions.firstOrNull { it.name == "pluginExperimentActivePlugin" }!!.annotations
148+
.firstOrNull { it.annotationClass == Toggle.InternalAlwaysEnabled::class },
149+
)
150+
assertTrue(clazz.kotlin.java.methods.find { it.name == "self" }!!.getAnnotation(Toggle.DefaultValue::class.java)!!.defaultValue)
151+
assertTrue(
152+
clazz.kotlin.java.methods.find { it.name == "pluginExperimentActivePlugin" }!!
153+
.getAnnotation(Toggle.DefaultValue::class.java)!!.defaultValue,
154+
)
155+
156+
val featureAnnotation = clazz.kotlin.java.getAnnotation(ContributesRemoteFeature::class.java)!!
157+
assertEquals(AppScope::class, featureAnnotation.scope)
158+
assertEquals("pluginPointMyPlugin", featureAnnotation.featureName)
159+
val expectedClass = Class
160+
.forName("com.duckduckgo.feature.toggles.codegen.ExperimentActivePlugin_ActivePlugin_RemoteFeature_MultiProcessStore")
161+
assertEquals(expectedClass.kotlin, featureAnnotation.toggleStore)
162+
}
163+
164+
@Test
165+
fun `test generated internal-always-enabled remote features`() {
166+
val clazz = Class
167+
.forName("com.duckduckgo.feature.toggles.codegen.InternalAlwaysEnabledActivePlugin_ActivePlugin_RemoteFeature")
168+
169+
assertNotNull(clazz.methods.find { it.name == "self" && it.returnType.kotlin == Toggle::class })
170+
assertNotNull(clazz.methods.find { it.name == "pluginInternalAlwaysEnabledActivePlugin" && it.returnType.kotlin == Toggle::class })
171+
172+
assertNotNull(
173+
clazz.kotlin.functions.firstOrNull { it.name == "self" }!!.annotations.firstOrNull { it.annotationClass == Toggle.DefaultValue::class },
174+
)
175+
assertNull(
176+
clazz.kotlin.functions.firstOrNull { it.name == "self" }!!.annotations.firstOrNull { it.annotationClass == Toggle.Experiment::class },
177+
)
178+
assertNull(
179+
clazz.kotlin.functions.firstOrNull { it.name == "self" }!!
180+
.annotations.firstOrNull { it.annotationClass == Toggle.InternalAlwaysEnabled::class },
181+
)
182+
assertNotNull(
183+
clazz.kotlin.functions.firstOrNull { it.name == "pluginInternalAlwaysEnabledActivePlugin" }!!.annotations
184+
.firstOrNull { it.annotationClass == Toggle.DefaultValue::class },
185+
)
186+
assertNull(
187+
clazz.kotlin.functions.firstOrNull { it.name == "pluginInternalAlwaysEnabledActivePlugin" }!!.annotations
188+
.firstOrNull { it.annotationClass == Toggle.Experiment::class },
189+
)
190+
assertNotNull(
191+
clazz.kotlin.functions.firstOrNull { it.name == "pluginInternalAlwaysEnabledActivePlugin" }!!.annotations
192+
.firstOrNull { it.annotationClass == Toggle.InternalAlwaysEnabled::class },
193+
)
194+
assertTrue(clazz.kotlin.java.methods.find { it.name == "self" }!!.getAnnotation(Toggle.DefaultValue::class.java)!!.defaultValue)
195+
assertTrue(
196+
clazz.kotlin.java.methods.find { it.name == "pluginInternalAlwaysEnabledActivePlugin" }!!
197+
.getAnnotation(Toggle.DefaultValue::class.java)!!.defaultValue,
198+
)
199+
200+
val featureAnnotation = clazz.kotlin.java.getAnnotation(ContributesRemoteFeature::class.java)!!
201+
assertEquals(AppScope::class, featureAnnotation.scope)
202+
assertEquals("pluginPointMyPlugin", featureAnnotation.featureName)
203+
val expectedClass = Class
204+
.forName("com.duckduckgo.feature.toggles.codegen.InternalAlwaysEnabledActivePlugin_ActivePlugin_RemoteFeature_MultiProcessStore")
205+
assertEquals(expectedClass.kotlin, featureAnnotation.toggleStore)
206+
}
207+
117208
@Test
118209
fun `test generated plugin point`() {
119210
val clazz = Class

feature-toggles/feature-toggles-impl/src/test/java/com/duckduckgo/feature/toggles/codegen/TestActivePlugins.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,26 @@ class BazActivePlugin @Inject constructor() : MyPlugin {
7878
override fun doSomething() {
7979
}
8080
}
81+
82+
@ContributesActivePlugin(
83+
scope = AppScope::class,
84+
boundType = MyPlugin::class,
85+
priority = 50,
86+
supportExperiments = true,
87+
)
88+
class ExperimentActivePlugin @Inject constructor() : MyPlugin {
89+
override fun doSomething() {
90+
}
91+
}
92+
93+
@ContributesActivePlugin(
94+
scope = AppScope::class,
95+
boundType = MyPlugin::class,
96+
priority = 50,
97+
internalAlwaysEnabled = true,
98+
supportExperiments = false,
99+
)
100+
class InternalAlwaysEnabledActivePlugin @Inject constructor() : MyPlugin {
101+
override fun doSomething() {
102+
}
103+
}

new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/NewTabPage.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import javax.inject.Inject
3030
boundType = NewTabPagePlugin::class,
3131
priority = 0, // lower to come first in the list of plugins,
3232
defaultActiveValue = false,
33+
supportExperiments = true,
34+
internalAlwaysEnabled = true,
3335
)
3436
class NewTabPage @Inject constructor() : NewTabPagePlugin {
3537

0 commit comments

Comments
 (0)