Skip to content

Commit e44964e

Browse files
committed
Bug 1997373 - Store original gecko pref values for Fenix Gecko integration
This patch begins storing a `Vec<PreviousGeckoPrefState>` on `ExperimentEnrollment` when it is of type `EnrollmentStatus::Enrolled` for managing the original value of gecko prefs used in an experiment. The public APIs it opens are `registerPreviousGeckoPrefStates` and `getPreviousGeckoPrefStates`.
1 parent f46182e commit e44964e

File tree

14 files changed

+846
-20
lines changed

14 files changed

+846
-20
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 `PreviousGeckoPrefState` on `ExperimentEnrollment` when it is of type `EnrollmentStatus::Enrolled` and getters and setters. This is to support returning to an original value on 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.PreviousGeckoPrefState
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 getPreviousGeckoPrefStates(experimentSlug: String): List<PreviousGeckoPrefState>? {
451+
return nimbusClient.getPreviousGeckoPrefStates(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.PreviousGeckoPrefState
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 list of previous states, if available on an enrolled experiment, from a given slug.
207+
*
208+
* @param experimentSlug The slug of the experiment.
209+
* @return The previous Gecko pref state of the given slug. Will return null if not available or invalid slug.
210+
*/
211+
fun getPreviousGeckoPrefStates(
212+
experimentSlug: String,
213+
): List<PreviousGeckoPrefState>? = 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: 36 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.PreviousGeckoPrefState
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,37 @@ 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 previousStates = nimbus.getPreviousGeckoPrefStates("test-experiment")
941+
shadowOf(Looper.getMainLooper()).idle()
942+
943+
assertNotNull(previousStates)
944+
val geckoPreviousStates = previousStates as List<PreviousGeckoPrefState>
945+
assertEquals(1, geckoPreviousStates.size)
946+
assertEquals("1", geckoPreviousStates[0].originalValue.value)
947+
assertEquals("pref.number", geckoPreviousStates[0].originalValue.pref)
948+
}
914949
}
915950

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

components/nimbus/src/enrollment.rs

Lines changed: 39 additions & 7 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::PrefUnenrollReason;
5+
use crate::stateful::gecko_prefs::{OriginalGeckoPref, PrefUnenrollReason};
66
use crate::{
77
defaults::Defaults,
88
error::{debug, warn, NimbusError, Result},
@@ -21,7 +21,7 @@ pub(crate) const PREVIOUS_ENROLLMENTS_GC_TIME: Duration = Duration::from_secs(36
2121

2222
// These are types we use internally for managing enrollments.
2323
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
24-
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
24+
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
2525
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
2626
pub enum EnrolledReason {
2727
/// A normal enrollment as per the experiment's rules.
@@ -45,7 +45,7 @@ impl Display for EnrolledReason {
4545
// These are types we use internally for managing non-enrollments.
4646

4747
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
48-
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
48+
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
4949
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
5050
pub enum NotEnrolledReason {
5151
/// The experiment targets a different application.
@@ -99,7 +99,7 @@ impl Default for Participation {
9999
// These are types we use internally for managing disqualifications.
100100

101101
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
102-
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
102+
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
103103
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
104104
pub enum DisqualifiedReason {
105105
/// There was an error.
@@ -134,10 +134,20 @@ impl Display for DisqualifiedReason {
134134
}
135135
}
136136

137-
// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
137+
// The previous state of a Gecko pref before enrollment took place.
138+
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
139+
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
140+
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
141+
#[cfg(feature = "stateful")]
142+
pub struct PreviousGeckoPrefState {
143+
pub original_value: OriginalGeckoPref,
144+
pub feature_id: String,
145+
pub variable: String,
146+
}
138147

148+
// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
139149
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
140-
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
150+
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
141151
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
142152
pub struct ExperimentEnrollment {
143153
pub slug: String,
@@ -430,6 +440,23 @@ impl ExperimentEnrollment {
430440
}
431441
}
432442

443+
// Previous gecko pref state is only settable on Enrolled experiments
444+
#[cfg(feature = "stateful")]
445+
pub(crate) fn on_add_gecko_pref_state(
446+
&self,
447+
prev_gecko_pref_states: Vec<PreviousGeckoPrefState>,
448+
) -> ExperimentEnrollment {
449+
let mut next = self.clone();
450+
if let EnrollmentStatus::Enrolled { reason, branch, .. } = &self.status {
451+
next.status = EnrollmentStatus::Enrolled {
452+
prev_gecko_pref_states: Some(prev_gecko_pref_states),
453+
reason: reason.clone(),
454+
branch: branch.clone(),
455+
};
456+
}
457+
next
458+
}
459+
433460
/// Reset identifiers in response to application-level telemetry reset.
434461
///
435462
/// We move any enrolled experiments to the "disqualified" state, since their further
@@ -531,12 +558,15 @@ impl ExperimentEnrollment {
531558
}
532559

533560
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
534-
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
561+
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
535562
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
536563
pub enum EnrollmentStatus {
537564
Enrolled {
538565
reason: EnrolledReason,
539566
branch: String,
567+
#[cfg(feature = "stateful")]
568+
#[serde(skip_serializing_if = "Option::is_none")]
569+
prev_gecko_pref_states: Option<Vec<PreviousGeckoPrefState>>,
540570
},
541571
NotEnrolled {
542572
reason: NotEnrolledReason,
@@ -577,6 +607,8 @@ impl EnrollmentStatus {
577607
EnrollmentStatus::Enrolled {
578608
reason,
579609
branch: branch.to_owned(),
610+
#[cfg(feature = "stateful")]
611+
prev_gecko_pref_states: None,
580612
}
581613
}
582614

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::PreviousGeckoPrefState;
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 prev_gecko_pref_states: Option<Vec<PreviousGeckoPrefState>>,
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+
prev_gecko_pref_states: None,
96104
}
97105
}
98106
}

components/nimbus/src/nimbus.udl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ dictionary EnrollmentStatusExtraDef {
101101
string? reason;
102102
string? slug;
103103
string? status;
104+
sequence<PreviousGeckoPrefState>? prev_gecko_pref_states;
105+
};
106+
107+
108+
dictionary PreviousGeckoPrefState {
109+
OriginalGeckoPref original_value;
110+
string feature_id;
111+
string variable;
104112
};
105113

106114
dictionary FeatureExposureExtraDef {
@@ -139,12 +147,20 @@ enum PrefBranch {
139147
"User",
140148
};
141149

150+
dictionary OriginalGeckoPref {
151+
string pref;
152+
PrefBranch branch;
153+
PrefValue? value;
154+
};
155+
156+
142157
enum PrefUnenrollReason {
143158
"Changed",
144159
"FailedToSet",
145160
};
146161

147162
dictionary PrefEnrollmentData {
163+
string experiment_slug;
148164
PrefValue pref_value;
149165
string feature_id;
150166
string variable;
@@ -362,6 +378,11 @@ interface NimbusClient {
362378
[Throws=NimbusError]
363379
sequence<EnrollmentChangeEvent> unenroll_for_gecko_pref(GeckoPrefState pref_state, PrefUnenrollReason pref_unenroll_reason);
364380

381+
[Throws=NimbusError]
382+
void register_previous_gecko_pref_states([ByRef] sequence<GeckoPrefState> gecko_pref_states);
383+
384+
[Throws=NimbusError]
385+
sequence<PreviousGeckoPrefState>? get_previous_gecko_pref_states(string experiment_slug);
365386
};
366387

367388
interface NimbusTargetingHelper {

0 commit comments

Comments
 (0)