Skip to content

Commit ba07d8d

Browse files
committed
Bug 2003370 - Use and react to Gecko preferences stored values
This patch adds functionality to use the new PreviousGeckoPrefState. It adds: * `set_gecko_prefs_original_values` for external handling of setting prefs back to original values * Mechanisms to return to a previous states when: * `on_experiment_updated` * Certain situations and as determined in will_pref_experiment_change * `on_experiment_ended` * `on_opt_out`
1 parent 0d78003 commit ba07d8d

File tree

10 files changed

+774
-49
lines changed

10 files changed

+774
-49
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.mozilla.experiments.nimbus.internal.GeckoPrefHandler
4343
import org.mozilla.experiments.nimbus.internal.GeckoPrefState
4444
import org.mozilla.experiments.nimbus.internal.JsonObject
4545
import org.mozilla.experiments.nimbus.internal.NimbusException
46+
import org.mozilla.experiments.nimbus.internal.OriginalGeckoPref
4647
import org.mozilla.experiments.nimbus.internal.PrefBranch
4748
import org.mozilla.experiments.nimbus.internal.PrefEnrollmentData
4849
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
@@ -860,6 +861,7 @@ class NimbusTests {
860861
),
861862
),
862863
var setValues: List<GeckoPrefState>? = null,
864+
var originalGeckoPrefValues: List<OriginalGeckoPref>? = null,
863865
) : GeckoPrefHandler {
864866
override fun getPrefsWithState(): Map<String, Map<String, GeckoPrefState>> {
865867
return internalMap
@@ -868,6 +870,10 @@ class NimbusTests {
868870
override fun setGeckoPrefsState(newPrefsState: List<GeckoPrefState>) {
869871
setValues = newPrefsState
870872
}
873+
874+
override fun setGeckoPrefsOriginalValues(originalGeckoPrefs: List<OriginalGeckoPref>) {
875+
originalGeckoPrefValues = originalGeckoPrefs
876+
}
871877
}
872878

873879
@Test
@@ -889,6 +895,21 @@ class NimbusTests {
889895
assertEquals("42", handler.setValues?.get(0)?.enrollmentValue?.prefValue)
890896
}
891897

898+
@Test
899+
fun `GeckoPrefHandler setGeckoPrefsOriginalValues function`() {
900+
val handler = TestGeckoPrefHandler()
901+
val originalValues = listOf(
902+
OriginalGeckoPref(
903+
pref = "pref.number",
904+
branch = PrefBranch.DEFAULT,
905+
value = "1",
906+
),
907+
)
908+
handler.setGeckoPrefsOriginalValues(originalValues)
909+
assertEquals(1, handler.originalGeckoPrefValues?.size)
910+
assertEquals("pref.number", handler.originalGeckoPrefValues?.get(0)?.pref)
911+
}
912+
892913
@Test
893914
fun `unenroll for gecko pref functions`() {
894915
val handler = TestGeckoPrefHandler()

components/nimbus/src/enrollment.rs

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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/.
44
#[cfg(feature = "stateful")]
5-
use crate::stateful::gecko_prefs::{OriginalGeckoPref, PrefUnenrollReason};
5+
use crate::stateful::gecko_prefs::{GeckoPrefStore, OriginalGeckoPref, PrefUnenrollReason};
66
use crate::{
77
defaults::Defaults,
88
error::{debug, warn, NimbusError, Result},
@@ -11,6 +11,8 @@ use crate::{
1111
SLUG_REPLACEMENT_PATTERN,
1212
};
1313
use serde_derive::*;
14+
#[cfg(feature = "stateful")]
15+
use std::sync::Arc;
1416
use std::{
1517
collections::{HashMap, HashSet},
1618
fmt::{Display, Formatter, Result as FmtResult},
@@ -145,6 +147,24 @@ pub struct PreviousGeckoPrefState {
145147
pub variable: String,
146148
}
147149

150+
#[cfg(feature = "stateful")]
151+
impl PreviousGeckoPrefState {
152+
pub(crate) fn on_revert_to_prev_gecko_pref_states(
153+
prev_gecko_pref_states: &[Self],
154+
gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
155+
) {
156+
if let Some(store) = gecko_pref_store {
157+
let original_values: Vec<_> = prev_gecko_pref_states
158+
.iter()
159+
.map(|state| state.original_value.clone())
160+
.collect();
161+
store
162+
.handler
163+
.set_gecko_prefs_original_values(original_values);
164+
}
165+
}
166+
}
167+
148168
// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
149169
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
150170
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
@@ -232,6 +252,7 @@ impl ExperimentEnrollment {
232252
available_randomization_units: &AvailableRandomizationUnits,
233253
updated_experiment: &Experiment,
234254
targeting_helper: &NimbusTargetingHelper,
255+
#[cfg(feature = "stateful")] gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
235256
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>,
236257
) -> Result<Self> {
237258
Ok(match &self.status {
@@ -265,12 +286,34 @@ impl ExperimentEnrollment {
265286
"Existing experiment enrollment '{}' is now disqualified (global opt-out)",
266287
&self.slug
267288
);
289+
#[cfg(feature = "stateful")]
290+
if let EnrollmentStatus::Enrolled {
291+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
292+
..
293+
} = &self.status
294+
{
295+
PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states(
296+
prev_gecko_pref_states,
297+
gecko_pref_store,
298+
);
299+
}
268300
let updated_enrollment =
269301
self.disqualify_from_enrolled(DisqualifiedReason::OptOut);
270302
out_enrollment_events.push(updated_enrollment.get_change_event());
271303
updated_enrollment
272304
} else if !updated_experiment.has_branch(branch) {
273305
// The branch we were in disappeared!
306+
#[cfg(feature = "stateful")]
307+
if let EnrollmentStatus::Enrolled {
308+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
309+
..
310+
} = &self.status
311+
{
312+
PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states(
313+
prev_gecko_pref_states,
314+
gecko_pref_store,
315+
);
316+
}
274317
let updated_enrollment =
275318
self.disqualify_from_enrolled(DisqualifiedReason::Error);
276319
out_enrollment_events.push(updated_enrollment.get_change_event());
@@ -285,6 +328,20 @@ impl ExperimentEnrollment {
285328
updated_experiment,
286329
targeting_helper,
287330
)?;
331+
332+
#[cfg(feature = "stateful")]
333+
if self.will_pref_experiment_change(updated_experiment, &evaluated_enrollment) {
334+
if let EnrollmentStatus::Enrolled {
335+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
336+
..
337+
} = &self.status
338+
{
339+
PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states(
340+
prev_gecko_pref_states,
341+
gecko_pref_store,
342+
);
343+
}
344+
}
288345
match evaluated_enrollment.status {
289346
EnrollmentStatus::Error { .. } => {
290347
let updated_enrollment =
@@ -369,6 +426,7 @@ impl ExperimentEnrollment {
369426
/// from the database after `PREVIOUS_ENROLLMENTS_GC_TIME`.
370427
fn on_experiment_ended(
371428
&self,
429+
#[cfg(feature = "stateful")] gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
372430
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>,
373431
) -> Option<Self> {
374432
debug!(
@@ -382,6 +440,17 @@ impl ExperimentEnrollment {
382440
| EnrollmentStatus::WasEnrolled { .. }
383441
| EnrollmentStatus::Error { .. } => return None, // We were never enrolled anyway, simply delete the enrollment record from the DB.
384442
};
443+
#[cfg(feature = "stateful")]
444+
if let EnrollmentStatus::Enrolled {
445+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
446+
..
447+
} = &self.status
448+
{
449+
PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states(
450+
prev_gecko_pref_states,
451+
gecko_pref_store,
452+
);
453+
}
385454
let enrollment = Self {
386455
slug: self.slug.clone(),
387456
status: EnrollmentStatus::WasEnrolled {
@@ -399,9 +468,22 @@ impl ExperimentEnrollment {
399468
pub(crate) fn on_explicit_opt_out(
400469
&self,
401470
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>,
471+
#[cfg(feature = "stateful")] gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
402472
) -> ExperimentEnrollment {
403473
match self.status {
404474
EnrollmentStatus::Enrolled { .. } => {
475+
#[cfg(feature = "stateful")]
476+
if let EnrollmentStatus::Enrolled {
477+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
478+
..
479+
} = &self.status
480+
{
481+
PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states(
482+
prev_gecko_pref_states,
483+
gecko_pref_store,
484+
);
485+
}
486+
405487
let enrollment = self.disqualify_from_enrolled(DisqualifiedReason::OptOut);
406488
out_enrollment_events.push(enrollment.get_change_event());
407489
enrollment
@@ -555,6 +637,71 @@ impl ExperimentEnrollment {
555637
| EnrollmentStatus::Error { .. } => self.clone(),
556638
}
557639
}
640+
641+
#[cfg(feature = "stateful")]
642+
pub(crate) fn will_pref_experiment_change(
643+
&self,
644+
updated_experiment: &Experiment,
645+
updated_enrollment: &ExperimentEnrollment,
646+
) -> bool {
647+
let (original_prev_gecko_pref_states, original_branch_slug) = match &self.status {
648+
EnrollmentStatus::Enrolled {
649+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
650+
branch,
651+
..
652+
} => (prev_gecko_pref_states, branch),
653+
// Can't change if it isn't a pref experiment
654+
_ => {
655+
return false;
656+
}
657+
};
658+
659+
let updated_branch_slug = match &updated_enrollment.status {
660+
EnrollmentStatus::Enrolled { branch, .. } => branch,
661+
// If we are no longer going to be enrolled, then a change happened
662+
_ => {
663+
return true;
664+
}
665+
};
666+
667+
// Branch changed
668+
if updated_branch_slug != original_branch_slug {
669+
return true;
670+
}
671+
672+
// Couldn't get a branch, something changed
673+
let Some(updated_branch) = updated_experiment.get_branch(updated_branch_slug) else {
674+
return true;
675+
};
676+
677+
let updated_features = updated_branch.get_feature_configs();
678+
let original_feature_ids: HashSet<&String> = original_prev_gecko_pref_states
679+
.iter()
680+
.map(|state| &state.feature_id)
681+
.collect();
682+
683+
// Amount of features should be the same
684+
if updated_features.len() != original_feature_ids.len() {
685+
return true;
686+
}
687+
688+
for original_state in original_prev_gecko_pref_states {
689+
let matching_feature = updated_features
690+
.iter()
691+
.find(|config| config.feature_id == original_state.feature_id);
692+
693+
// If original feature isn't present, then something changed
694+
let Some(updated_feature) = matching_feature else {
695+
return true;
696+
};
697+
698+
// Property key should still exist in the feature's value map
699+
if !updated_feature.value.contains_key(&original_state.variable) {
700+
return true;
701+
}
702+
}
703+
false
704+
}
558705
}
559706

560707
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
@@ -650,6 +797,7 @@ impl<'a> EnrollmentsEvolver<'a> {
650797
prev_experiments: &[E],
651798
next_experiments: &[Experiment],
652799
prev_enrollments: &[ExperimentEnrollment],
800+
#[cfg(feature = "stateful")] gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
653801
) -> Result<(Vec<ExperimentEnrollment>, Vec<EnrollmentChangeEvent>)>
654802
where
655803
E: ExperimentMetadata + Clone,
@@ -671,6 +819,8 @@ impl<'a> EnrollmentsEvolver<'a> {
671819
&prev_rollouts,
672820
&next_rollouts,
673821
&ro_enrollments,
822+
#[cfg(feature = "stateful")]
823+
gecko_pref_store,
674824
)?;
675825

676826
enrollments.extend(next_ro_enrollments);
@@ -695,6 +845,8 @@ impl<'a> EnrollmentsEvolver<'a> {
695845
&prev_experiments,
696846
&next_experiments,
697847
&prev_enrollments,
848+
#[cfg(feature = "stateful")]
849+
gecko_pref_store,
698850
)?;
699851

700852
enrollments.extend(next_exp_enrollments);
@@ -711,6 +863,7 @@ impl<'a> EnrollmentsEvolver<'a> {
711863
prev_experiments: &[E],
712864
next_experiments: &[Experiment],
713865
prev_enrollments: &[ExperimentEnrollment],
866+
#[cfg(feature = "stateful")] gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
714867
) -> Result<(Vec<ExperimentEnrollment>, Vec<EnrollmentChangeEvent>)>
715868
where
716869
E: ExperimentMetadata + Clone,
@@ -750,6 +903,8 @@ impl<'a> EnrollmentsEvolver<'a> {
750903
next_experiments_map.get(slug).copied(),
751904
Some(prev_enrollment),
752905
&mut enrollment_events,
906+
#[cfg(feature = "stateful")]
907+
gecko_pref_store,
753908
) {
754909
Ok(enrollment) => enrollment,
755910
Err(e) => {
@@ -851,6 +1006,8 @@ impl<'a> EnrollmentsEvolver<'a> {
8511006
Some(next_experiment),
8521007
prev_enrollment,
8531008
&mut enrollment_events,
1009+
#[cfg(feature = "stateful")]
1010+
gecko_pref_store,
8541011
) {
8551012
Ok(enrollment) => enrollment,
8561013
Err(e) => {
@@ -944,6 +1101,7 @@ impl<'a> EnrollmentsEvolver<'a> {
9441101
next_experiment: Option<&Experiment>,
9451102
prev_enrollment: Option<&ExperimentEnrollment>,
9461103
out_enrollment_events: &mut Vec<EnrollmentChangeEvent>, // out param containing the events we'd like to emit to glean.
1104+
#[cfg(feature = "stateful")] gecko_pref_store: &Option<Arc<GeckoPrefStore>>,
9471105
) -> Result<Option<ExperimentEnrollment>>
9481106
where
9491107
E: ExperimentMetadata + Clone,
@@ -972,16 +1130,20 @@ impl<'a> EnrollmentsEvolver<'a> {
9721130
out_enrollment_events,
9731131
)?),
9741132
// Experiment deleted remotely.
975-
(Some(_), None, Some(enrollment)) => {
976-
enrollment.on_experiment_ended(out_enrollment_events)
977-
}
1133+
(Some(_), None, Some(enrollment)) => enrollment.on_experiment_ended(
1134+
#[cfg(feature = "stateful")]
1135+
gecko_pref_store,
1136+
out_enrollment_events,
1137+
),
9781138
// Known experiment.
9791139
(Some(_), Some(experiment), Some(enrollment)) => {
9801140
Some(enrollment.on_experiment_updated(
9811141
is_user_participating,
9821142
self.available_randomization_units,
9831143
experiment,
9841144
&targeting_helper,
1145+
#[cfg(feature = "stateful")]
1146+
gecko_pref_store,
9851147
out_enrollment_events,
9861148
)?)
9871149
}

components/nimbus/src/nimbus.udl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ callback interface GeckoPrefHandler {
128128
record<string, record<string, GeckoPrefState>> get_prefs_with_state();
129129

130130
void set_gecko_prefs_state(sequence<GeckoPrefState> new_prefs_state);
131+
132+
void set_gecko_prefs_original_values(sequence<OriginalGeckoPref> original_gecko_prefs);
133+
131134
};
132135

133136
dictionary GeckoPref {

0 commit comments

Comments
 (0)