Skip to content

Commit 35e8cc9

Browse files
committed
EXP-5700: Change from global rollout opt-out to experiments and rollouts separately (#6888)
- Adding db migration from v2 to v3 due to the flag change - Removing the old set_global_rollout function - Adding set_experiment and set_rollout functions - Export four new functions via UDL - get_rollouts_user_participation - set_rollouts_user_participation - get_experiments_user_participation - set_experiments_user_participation Marked `set_global_user_participation` and `get_global_user_participation` as deprecated
1 parent 433952a commit 35e8cc9

File tree

25 files changed

+713
-163
lines changed

25 files changed

+713
-163
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/nimbus/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "nimbus-sdk"
3-
version = "0.10.0"
3+
version = "0.10.1"
44
authors = ["The Glean Team <glean-team@mozilla.com>", "The Sync Team <sync-team@mozilla.com>"]
55
edition = "2021"
66
description = "A rapid experiment library"

components/nimbus/android/src/main/java/org/mozilla/experiments/nimbus/Nimbus.kt

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,31 @@ open class Nimbus(
141141

142142
private val nimbusClient: NimbusClientInterface
143143

144+
override var experimentParticipation: Boolean
145+
get() = nimbusClient.getExperimentParticipation()
146+
set(active) {
147+
dbScope.launch {
148+
nimbusClient.setExperimentParticipation(active)
149+
applyPendingExperimentsOnThisThread()
150+
}
151+
}
152+
153+
override var rolloutParticipation: Boolean
154+
get() = nimbusClient.getRolloutParticipation()
155+
set(active) {
156+
dbScope.launch {
157+
nimbusClient.setRolloutParticipation(active)
158+
applyPendingExperimentsOnThisThread()
159+
}
160+
}
161+
162+
@Deprecated("Use experimentParticipation and rolloutParticipation instead")
144163
override var globalUserParticipation: Boolean
145164
get() = nimbusClient.getGlobalUserParticipation()
146165
set(active) {
147166
dbScope.launch {
148-
setGlobalUserParticipationOnThisThread(active)
167+
setExperimentParticipationOnThisThread(active)
168+
setRolloutParticipationOnThisThread(active)
149169
}
150170
}
151171

@@ -400,13 +420,37 @@ open class Nimbus(
400420

401421
@WorkerThread
402422
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
403-
internal fun setGlobalUserParticipationOnThisThread(active: Boolean) = withCatchAll("setGlobalUserParticipation") {
404-
val enrolmentChanges = nimbusClient.setGlobalUserParticipation(active)
405-
if (enrolmentChanges.isNotEmpty()) {
406-
recordExperimentTelemetryEvents(enrolmentChanges)
407-
postEnrolmentCalculation()
423+
internal fun setExperimentParticipationOnThisThread(active: Boolean) =
424+
withCatchAll("setExperimentParticipation") {
425+
val enrolmentChanges = nimbusClient.setExperimentParticipation(active)
426+
if (enrolmentChanges.isNotEmpty()) {
427+
recordExperimentTelemetryEvents(enrolmentChanges)
428+
postEnrolmentCalculation()
429+
}
430+
}
431+
432+
@WorkerThread
433+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
434+
internal fun setRolloutParticipationOnThisThread(active: Boolean) =
435+
withCatchAll("setRolloutParticipation") {
436+
val enrolmentChanges = nimbusClient.setRolloutParticipation(active)
437+
if (enrolmentChanges.isNotEmpty()) {
438+
recordExperimentTelemetryEvents(enrolmentChanges)
439+
postEnrolmentCalculation()
440+
}
441+
}
442+
443+
@Deprecated("Use setExperimentParticipationOnThisThread and setRolloutParticipationOnThisThread instead")
444+
@WorkerThread
445+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
446+
internal fun setGlobalUserParticipationOnThisThread(active: Boolean) =
447+
withCatchAll("setGlobalUserParticipation") {
448+
val enrolmentChanges = nimbusClient.setGlobalUserParticipation(active)
449+
if (enrolmentChanges.isNotEmpty()) {
450+
recordExperimentTelemetryEvents(enrolmentChanges)
451+
postEnrolmentCalculation()
452+
}
408453
}
409-
}
410454

411455
override fun optOut(experimentId: String) {
412456
dbScope.launch {

components/nimbus/android/src/main/java/org/mozilla/experiments/nimbus/NimbusInterface.kt

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,28 @@ interface NimbusInterface : FeaturesInterface, NimbusMessagingInterface, NimbusE
198198
*/
199199
fun resetTelemetryIdentifiers() = Unit
200200

201-
/**
202-
* Control the opt out for all experiments at once. This is likely a user action.
203-
*/
201+
@Deprecated("Use experimentParticipation and rolloutParticipation instead")
204202
var globalUserParticipation: Boolean
205-
get() = false
203+
get() = experimentParticipation && rolloutParticipation
204+
set(value) {
205+
experimentParticipation = value
206+
rolloutParticipation = value
207+
}
208+
209+
/**
210+
* Control the opt out for experiments. This is likely a user action.
211+
* When set to false, the user will be opted out of all experiments but not rollouts.
212+
*/
213+
var experimentParticipation: Boolean
214+
get() = true
215+
set(_) = Unit
216+
217+
/**
218+
* Control the opt out for rollouts. This is likely a user action.
219+
* When set to false, the user will be opted out of all rollouts but not experiments.
220+
*/
221+
var rolloutParticipation: Boolean
222+
get() = true
206223
set(_) = Unit
207224

208225
override val events: NimbusEventStore

components/nimbus/android/src/test/java/org/mozilla/experiments/nimbus/NimbusTests.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,8 @@ class NimbusTests {
450450
NimbusEvents.disqualification.testGetValue(),
451451
)
452452

453-
// Opt out of all experiments
454-
nimbus.setGlobalUserParticipationOnThisThread(false)
453+
// Opt out of experiments but keep rollouts
454+
nimbus.setExperimentParticipationOnThisThread(false)
455455

456456
// Use the Glean test API to check that the valid event is present
457457
assertNotNull("Event must have a value", NimbusEvents.disqualification.testGetValue())

components/nimbus/src/enrollment.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ impl Display for NotEnrolledReason {
7575
}
7676
}
7777

78+
#[derive(Serialize, Deserialize, Debug, Clone)]
79+
pub struct Participation {
80+
pub in_experiments: bool,
81+
pub in_rollouts: bool,
82+
}
83+
84+
impl Default for Participation {
85+
fn default() -> Self {
86+
Self {
87+
in_experiments: true,
88+
in_rollouts: true,
89+
}
90+
}
91+
}
92+
7893
// These are types we use internally for managing disqualifications.
7994

8095
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
@@ -583,7 +598,7 @@ impl<'a> EnrollmentsEvolver<'a> {
583598

584599
pub(crate) fn evolve_enrollments<E>(
585600
&mut self,
586-
is_user_participating: bool,
601+
participation: Participation,
587602
prev_experiments: &[E],
588603
next_experiments: &[Experiment],
589604
prev_enrollments: &[ExperimentEnrollment],
@@ -604,7 +619,7 @@ impl<'a> EnrollmentsEvolver<'a> {
604619
let next_rollouts = filter_experiments(next_experiments, ExperimentMetadata::is_rollout);
605620

606621
let (next_ro_enrollments, ro_events) = self.evolve_enrollment_recipes(
607-
is_user_participating,
622+
participation.in_rollouts,
608623
&prev_rollouts,
609624
&next_rollouts,
610625
&ro_enrollments,
@@ -628,7 +643,7 @@ impl<'a> EnrollmentsEvolver<'a> {
628643
.collect();
629644

630645
let (next_exp_enrollments, exp_events) = self.evolve_enrollment_recipes(
631-
is_user_participating,
646+
participation.in_experiments,
632647
&prev_experiments,
633648
&next_experiments,
634649
&prev_enrollments,

components/nimbus/src/nimbus.udl

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,29 @@ interface NimbusClient {
239239
[Throws=NimbusError]
240240
sequence<AvailableExperiment> get_available_experiments();
241241

242-
/// Getter and setter for user's participation in all experiments.
242+
/// Getter and setter for user's participation in experiments only.
243243
/// Possible values are:
244-
/// * `true`: the user will not enroll in new experiments, and opt out of all existing ones.
245-
/// * `false`: experiments proceed as usual.
244+
/// * `true`: the user will enroll in experiments as usual.
245+
/// * `false`: the user will not enroll in new experiments, and opt out of all existing ones.
246+
[Throws=NimbusError]
247+
boolean get_experiment_participation();
248+
249+
[Throws=NimbusError]
250+
sequence<EnrollmentChangeEvent> set_experiment_participation(boolean opt_in);
251+
252+
/// Getter and setter for user's participation in rollouts.
253+
/// Possible values are:
254+
/// * `true`: the user will enroll in rollouts as usual.
255+
/// * `false`: the user will not enroll in new rollouts, and opt out of all existing ones.
256+
[Throws=NimbusError]
257+
boolean get_rollout_participation();
258+
259+
[Throws=NimbusError]
260+
sequence<EnrollmentChangeEvent> set_rollout_participation(boolean opt_in);
261+
262+
/// DEPRECATED: Use set_experiment_participation and set_rollout_participation instead.
263+
/// Getter and setter for global user participation (applies to both experiments and rollouts).
264+
/// For simplicity, the getter returns the experiment participation value.
246265
[Throws=NimbusError]
247266
boolean get_global_user_participation();
248267

@@ -258,7 +277,7 @@ interface NimbusClient {
258277
/// Toggles the enablement of the fetch. If `false`, then calling `fetch_experiments`
259278
/// returns immediately, having not done any fetching from remote settings.
260279
/// This is only useful for QA, and should not be used in production: use
261-
/// `set_global_user_participation` instead.
280+
/// `set_experiment_participation` or `set_rollout_participation` instead.
262281
[Throws=NimbusError]
263282
void set_fetch_enabled(boolean flag);
264283

components/nimbus/src/stateful/enrollment.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/* This Source Code Form is subject to the terms of the Mozilla Public
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
use crate::enrollment::Participation;
5+
use crate::stateful::persistence::{
6+
DB_KEY_EXPERIMENT_PARTICIPATION, DB_KEY_ROLLOUT_PARTICIPATION,
7+
DEFAULT_EXPERIMENT_PARTICIPATION, DEFAULT_ROLLOUT_PARTICIPATION,
8+
};
49
use crate::{
510
enrollment::{
611
map_enrollments, EnrollmentChangeEvent, EnrollmentChangeEventType, EnrollmentsEvolver,
@@ -14,9 +19,6 @@ use crate::{
1419
EnrolledExperiment, EnrollmentStatus, Experiment,
1520
};
1621

17-
const DB_KEY_GLOBAL_USER_PARTICIPATION: &str = "user-opt-in";
18-
const DEFAULT_GLOBAL_USER_PARTICIPATION: bool = true;
19-
2022
impl EnrollmentsEvolver<'_> {
2123
/// Convenient wrapper around `evolve_enrollments` that fetches the current state of experiments,
2224
/// enrollments and user participation from the database.
@@ -26,15 +28,22 @@ impl EnrollmentsEvolver<'_> {
2628
writer: &mut Writer,
2729
next_experiments: &[Experiment],
2830
) -> Result<Vec<EnrollmentChangeEvent>> {
29-
// Get the state from the db.
30-
let is_user_participating = get_global_user_participation(db, writer)?;
31+
// Get separate participation states from the db
32+
let is_participating_in_experiments = get_experiment_participation(db, writer)?;
33+
let is_participating_in_rollouts = get_rollout_participation(db, writer)?;
34+
35+
let participation = Participation {
36+
in_experiments: is_participating_in_experiments,
37+
in_rollouts: is_participating_in_rollouts,
38+
};
39+
3140
let experiments_store = db.get_store(StoreId::Experiments);
3241
let enrollments_store = db.get_store(StoreId::Enrollments);
3342
let prev_experiments: Vec<Experiment> = experiments_store.collect_all(writer)?;
3443
let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;
3544
// Calculate the changes.
3645
let (next_enrollments, enrollments_change_events) = self.evolve_enrollments(
37-
is_user_participating,
46+
participation,
3847
&prev_experiments,
3948
next_experiments,
4049
&prev_enrollments,
@@ -171,26 +180,41 @@ pub fn unenroll_for_pref(
171180
Ok(events)
172181
}
173182

174-
pub fn get_global_user_participation<'r>(
183+
pub fn get_experiment_participation<'r>(
175184
db: &Database,
176185
reader: &'r impl Readable<'r>,
177186
) -> Result<bool> {
178187
let store = db.get_store(StoreId::Meta);
179-
let opted_in = store.get::<bool, _>(reader, DB_KEY_GLOBAL_USER_PARTICIPATION)?;
188+
let opted_in = store.get::<bool, _>(reader, DB_KEY_EXPERIMENT_PARTICIPATION)?;
189+
if let Some(opted_in) = opted_in {
190+
Ok(opted_in)
191+
} else {
192+
Ok(DEFAULT_EXPERIMENT_PARTICIPATION)
193+
}
194+
}
195+
196+
pub fn get_rollout_participation<'r>(db: &Database, reader: &'r impl Readable<'r>) -> Result<bool> {
197+
let store = db.get_store(StoreId::Meta);
198+
let opted_in = store.get::<bool, _>(reader, DB_KEY_ROLLOUT_PARTICIPATION)?;
180199
if let Some(opted_in) = opted_in {
181200
Ok(opted_in)
182201
} else {
183-
Ok(DEFAULT_GLOBAL_USER_PARTICIPATION)
202+
Ok(DEFAULT_ROLLOUT_PARTICIPATION)
184203
}
185204
}
186205

187-
pub fn set_global_user_participation(
206+
pub fn set_experiment_participation(
188207
db: &Database,
189208
writer: &mut Writer,
190209
opt_in: bool,
191210
) -> Result<()> {
192211
let store = db.get_store(StoreId::Meta);
193-
store.put(writer, DB_KEY_GLOBAL_USER_PARTICIPATION, &opt_in)
212+
store.put(writer, DB_KEY_EXPERIMENT_PARTICIPATION, &opt_in)
213+
}
214+
215+
pub fn set_rollout_participation(db: &Database, writer: &mut Writer, opt_in: bool) -> Result<()> {
216+
let store = db.get_store(StoreId::Meta);
217+
store.put(writer, DB_KEY_ROLLOUT_PARTICIPATION, &opt_in)
194218
}
195219

196220
/// Reset unique identifiers in response to application-level telemetry reset.

0 commit comments

Comments
 (0)