Skip to content

Commit 181a21d

Browse files
committed
Bug 1997373 - Store original pref values for Fenix Gecko integration
This patch begins storing a `PreviousState` on `ExperimentEnrollment` when it is of type `EnrollmentStatus::Enrolled`. It also introduces `PreviousState::GeckoPref` to hold Gecko original preference values for Gecko pref based experiments. The public APIs it opens are `registerPreviousGeckoPrefStates` and `getPreviousState`.
1 parent caf03ed commit 181a21d

File tree

16 files changed

+848
-18
lines changed

16 files changed

+848
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# v148.0 (In progress)
2+
### Nimbus
3+
* Adds `PreviousState` on `ExperimentEnrollment` when it is of type `EnrollmentStatus::Enrolled` and getters and setters. `PreviousState::GeckoPref` is added to support previous states for Gecko pref experiments.
24

35
[Full Changelog](In progress)
46

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.mozilla.experiments.nimbus.internal.NimbusClient
4444
import org.mozilla.experiments.nimbus.internal.NimbusClientInterface
4545
import org.mozilla.experiments.nimbus.internal.NimbusException
4646
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
47+
import org.mozilla.experiments.nimbus.internal.PreviousState
4748
import org.mozilla.experiments.nimbus.internal.RecordedContext
4849
import java.io.File
4950
import java.io.IOException
@@ -438,6 +439,18 @@ open class Nimbus(
438439
return nimbusClient.unenrollForGeckoPref(geckoPrefState, prefUnenrollReason)
439440
}
440441

442+
override fun registerPreviousGeckoPrefStates(geckoPrefStates: List<GeckoPrefState>) {
443+
dbScope.launch {
444+
withCatchAll("registerPreviousGeckoPrefStates") {
445+
nimbusClient.registerPreviousGeckoPrefStates(geckoPrefStates)
446+
}
447+
}
448+
}
449+
450+
override fun getPreviousState(experimentSlug: String): PreviousState? {
451+
return nimbusClient.getPreviousState(experimentSlug)
452+
}
453+
441454
@WorkerThread
442455
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
443456
internal fun optOutOnThisThread(experimentId: String) = withCatchAll("optOut") {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.mozilla.experiments.nimbus.internal.EnrollmentChangeEvent
1717
import org.mozilla.experiments.nimbus.internal.ExperimentBranch
1818
import org.mozilla.experiments.nimbus.internal.GeckoPrefState
1919
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
20+
import org.mozilla.experiments.nimbus.internal.PreviousState
2021
import java.time.Duration
2122
import java.util.concurrent.TimeUnit
2223

@@ -191,6 +192,26 @@ interface NimbusInterface : FeaturesInterface, NimbusMessagingInterface, NimbusE
191192
prefUnenrollReason: PrefUnenrollReason,
192193
): List<EnrollmentChangeEvent> = listOf()
193194

195+
/**
196+
* Add the original Gecko pref values as a previous state on each involved enrollment.
197+
*
198+
* @param geckoPrefStates The list of items that should have their enrollment state updated with
199+
* original Gecko pref previous state information.
200+
*/
201+
fun registerPreviousGeckoPrefStates(
202+
geckoPrefStates: List<GeckoPrefState>,
203+
) = Unit
204+
205+
/**
206+
* Retrieves a previous state, if available on an enrolled experiment, from a given slug.
207+
*
208+
* @param experimentSlug The slug of the experiment.
209+
* @return The previous state of the given slug. Will return null if not available or invalid slug.
210+
*/
211+
fun getPreviousState(
212+
experimentSlug: String,
213+
): PreviousState? = null
214+
194215
/**
195216
* Reset internal state in response to application-level telemetry reset.
196217
* Consumers should call this method when the user resets the telemetry state of the

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.mozilla.experiments.nimbus
66

77
import android.content.Context
8+
import android.os.Looper
89
import android.util.Log
910
import androidx.test.core.app.ApplicationProvider
1011
import kotlinx.coroutines.CancellationException
@@ -43,11 +44,14 @@ import org.mozilla.experiments.nimbus.internal.GeckoPrefState
4344
import org.mozilla.experiments.nimbus.internal.JsonObject
4445
import org.mozilla.experiments.nimbus.internal.NimbusException
4546
import org.mozilla.experiments.nimbus.internal.PrefBranch
47+
import org.mozilla.experiments.nimbus.internal.PrefEnrollmentData
4648
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
49+
import org.mozilla.experiments.nimbus.internal.PreviousState
4750
import org.mozilla.experiments.nimbus.internal.RecordedContext
4851
import org.mozilla.experiments.nimbus.internal.getCalculatedAttributes
4952
import org.mozilla.experiments.nimbus.internal.validateEventQueries
5053
import org.robolectric.RobolectricTestRunner
54+
import org.robolectric.Shadows.shadowOf
5155
import java.io.File
5256
import java.util.Calendar
5357
import java.util.concurrent.Executors
@@ -849,7 +853,7 @@ class NimbusTests {
849853
"number" to GeckoPrefState(
850854
geckoPref = GeckoPref("pref.number", PrefBranch.DEFAULT),
851855
geckoValue = "1",
852-
enrollmentValue = null,
856+
enrollmentValue = PrefEnrollmentData("test-experiment", "42", "about_welcome", "number"),
853857
isUserSet = false,
854858
),
855859
),
@@ -911,6 +915,36 @@ class NimbusTests {
911915
assertEquals(EnrollmentChangeEventType.DISQUALIFICATION, events[0].change)
912916
assertEquals(0, handler.setValues?.size)
913917
}
918+
919+
@Test
920+
fun `register previous gecko states and check values`() {
921+
val handler = TestGeckoPrefHandler()
922+
923+
val nimbus = createNimbus(geckoPrefHandler = handler)
924+
925+
suspend fun getString(): String {
926+
return testExperimentsJsonString(appInfo, packageName)
927+
}
928+
929+
val job = nimbus.applyLocalExperiments(::getString)
930+
runBlocking {
931+
job.join()
932+
}
933+
934+
assertEquals(1, handler.setValues?.size)
935+
assertEquals("42", handler.setValues?.get(0)?.enrollmentValue?.prefValue)
936+
937+
nimbus.registerPreviousGeckoPrefStates(handler.setValues!!)
938+
shadowOf(Looper.getMainLooper()).idle()
939+
940+
val previousState = nimbus.getPreviousState("test-experiment")
941+
shadowOf(Looper.getMainLooper()).idle()
942+
943+
assertNotNull(previousState)
944+
val geckoPreviousState = previousState as PreviousState.GeckoPref
945+
assertEquals("1", geckoPreviousState.originalValues[0].prefValue)
946+
assertEquals("pref.number", geckoPreviousState.originalValues[0].pref)
947+
}
914948
}
915949

916950
// Mocking utilities, from mozilla.components.support.test

components/nimbus/src/enrollment.rs

Lines changed: 50 additions & 1 deletion
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::PrefUnenrollReason;
5+
use crate::stateful::gecko_prefs::{OriginalGeckoPref, PrefUnenrollReason};
66
use crate::{
77
defaults::Defaults,
88
error::{debug, warn, NimbusError, Result},
@@ -134,6 +134,34 @@ impl Display for DisqualifiedReason {
134134
}
135135
}
136136

137+
// The previous state of a feature before enrollment.
138+
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
139+
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
140+
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
141+
#[cfg(feature = "stateful")]
142+
pub enum PreviousState {
143+
GeckoPref {
144+
original_values: Vec<OriginalGeckoPref>,
145+
feature_id: String,
146+
variable: String,
147+
},
148+
}
149+
#[cfg(feature = "stateful")]
150+
impl Display for PreviousState {
151+
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
152+
match self {
153+
PreviousState::GeckoPref {
154+
original_values,
155+
feature_id,
156+
variable,
157+
} => write!(
158+
f,
159+
"original_values = {:?} feature_id = {} variable = {}",
160+
original_values, feature_id, variable
161+
),
162+
}
163+
}
164+
}
137165
// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
138166

139167
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
@@ -430,6 +458,22 @@ impl ExperimentEnrollment {
430458
}
431459
}
432460

461+
// Previous state is only settable on Enrolled experiments
462+
#[cfg(feature = "stateful")]
463+
pub(crate) fn on_add_state(&self, previous_state: PreviousState) -> ExperimentEnrollment {
464+
match self.status.clone() {
465+
EnrollmentStatus::Enrolled { reason, branch, .. } => ExperimentEnrollment {
466+
status: EnrollmentStatus::Enrolled {
467+
previous_state: Some(previous_state),
468+
reason,
469+
branch,
470+
},
471+
..self.clone()
472+
},
473+
_ => self.clone(),
474+
}
475+
}
476+
433477
/// Reset identifiers in response to application-level telemetry reset.
434478
///
435479
/// We move any enrolled experiments to the "disqualified" state, since their further
@@ -537,6 +581,9 @@ pub enum EnrollmentStatus {
537581
Enrolled {
538582
reason: EnrolledReason,
539583
branch: String,
584+
#[cfg(feature = "stateful")]
585+
#[serde(skip_serializing_if = "Option::is_none")]
586+
previous_state: Option<PreviousState>,
540587
},
541588
NotEnrolled {
542589
reason: NotEnrolledReason,
@@ -577,6 +624,8 @@ impl EnrollmentStatus {
577624
EnrollmentStatus::Enrolled {
578625
reason,
579626
branch: branch.to_owned(),
627+
#[cfg(feature = "stateful")]
628+
previous_state: None,
580629
}
581630
}
582631

components/nimbus/src/metrics.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
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

5+
#[cfg(feature = "stateful")]
6+
use crate::enrollment::PreviousState;
7+
58
use crate::{enrollment::ExperimentEnrollment, EnrolledFeature, EnrollmentStatus};
69
use serde_derive::{Deserialize, Serialize};
710

@@ -28,6 +31,9 @@ pub struct EnrollmentStatusExtraDef {
2831
pub status: Option<String>,
2932
#[cfg(not(feature = "stateful"))]
3033
pub user_id: Option<String>,
34+
#[cfg(feature = "stateful")]
35+
#[serde(skip_serializing_if = "Option::is_none")]
36+
pub previous_state: Option<PreviousState>,
3137
}
3238

3339
#[cfg(test)]
@@ -93,6 +99,8 @@ impl From<ExperimentEnrollment> for EnrollmentStatusExtraDef {
9399
status: Some(enrollment.status.name()),
94100
#[cfg(not(feature = "stateful"))]
95101
user_id: None,
102+
#[cfg(feature = "stateful")]
103+
previous_state: None,
96104
}
97105
}
98106
}

components/nimbus/src/nimbus.udl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ dictionary EnrollmentStatusExtraDef {
101101
string? reason;
102102
string? slug;
103103
string? status;
104+
PreviousState? previous_state;
105+
};
106+
107+
[Enum]
108+
interface PreviousState {
109+
GeckoPref(sequence<OriginalGeckoPref> original_values, string feature_id, string variable);
104110
};
105111

106112
dictionary FeatureExposureExtraDef {
@@ -139,12 +145,20 @@ enum PrefBranch {
139145
"User",
140146
};
141147

148+
dictionary OriginalGeckoPref {
149+
string pref;
150+
PrefBranch branch;
151+
PrefValue? pref_value;
152+
};
153+
154+
142155
enum PrefUnenrollReason {
143156
"Changed",
144157
"FailedToSet",
145158
};
146159

147160
dictionary PrefEnrollmentData {
161+
string experiment_slug;
148162
PrefValue pref_value;
149163
string feature_id;
150164
string variable;
@@ -362,6 +376,11 @@ interface NimbusClient {
362376
[Throws=NimbusError]
363377
sequence<EnrollmentChangeEvent> unenroll_for_gecko_pref(GeckoPrefState pref_state, PrefUnenrollReason pref_unenroll_reason);
364378

379+
[Throws=NimbusError]
380+
void register_previous_gecko_pref_states(sequence<GeckoPrefState> gecko_pref_states);
381+
382+
[Throws=NimbusError]
383+
PreviousState? get_previous_state(string experiment_slug);
365384
};
366385

367386
interface NimbusTargetingHelper {

components/nimbus/src/stateful/enrollment.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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;
4+
use crate::enrollment::{Participation, PreviousState};
55
use crate::stateful::persistence::{
66
DB_KEY_EXPERIMENT_PARTICIPATION, DB_KEY_ROLLOUT_PARTICIPATION,
77
DEFAULT_EXPERIMENT_PARTICIPATION, DEFAULT_ROLLOUT_PARTICIPATION,
@@ -180,6 +180,47 @@ pub fn unenroll_for_pref(
180180
Ok(events)
181181
}
182182

183+
pub fn add_previous_state_for_experiment(
184+
db: &Database,
185+
writer: &mut Writer,
186+
experiment_slug: &str,
187+
previous_state: PreviousState,
188+
) -> Result<()> {
189+
let enr_store = db.get_store(StoreId::Enrollments);
190+
191+
if let Ok(Some(existing_enrollment)) =
192+
enr_store.get::<ExperimentEnrollment, Writer>(writer, experiment_slug)
193+
{
194+
// Previous state is only valid on Enrolled experiments
195+
let updated_state = &existing_enrollment.on_add_state(previous_state);
196+
enr_store.put(writer, experiment_slug, updated_state)?;
197+
}
198+
Ok(())
199+
}
200+
201+
pub fn get_previous_state_for_experiment<'r>(
202+
db: &Database,
203+
reader: &'r impl Readable<'r>,
204+
experiment_slug: &str,
205+
) -> Result<Option<PreviousState>> {
206+
let enr_store = db.get_store(StoreId::Enrollments);
207+
208+
match enr_store.get::<ExperimentEnrollment, _>(reader, experiment_slug)? {
209+
Some(enrollment) => {
210+
if let EnrollmentStatus::Enrolled {
211+
previous_state: Some(previous_state),
212+
..
213+
} = enrollment.status
214+
{
215+
Ok(Some(previous_state))
216+
} else {
217+
Ok(None)
218+
}
219+
}
220+
None => Ok(None),
221+
}
222+
}
223+
183224
pub fn get_experiment_participation<'r>(
184225
db: &Database,
185226
reader: &'r impl Readable<'r>,

0 commit comments

Comments
 (0)