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 } ;
66use crate :: {
77 defaults:: Defaults ,
88 error:: { debug, warn, NimbusError , Result } ,
@@ -11,6 +11,8 @@ use crate::{
1111 SLUG_REPLACEMENT_PATTERN ,
1212} ;
1313use serde_derive:: * ;
14+ #[ cfg( feature = "stateful" ) ]
15+ use std:: sync:: Arc ;
1416use 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 }
0 commit comments