Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# v148.0 (In progress)
### Nimbus
* 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.

[Full Changelog](In progress)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import org.mozilla.experiments.nimbus.internal.NimbusClient
import org.mozilla.experiments.nimbus.internal.NimbusClientInterface
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
import org.mozilla.experiments.nimbus.internal.PreviousState
import org.mozilla.experiments.nimbus.internal.RecordedContext
import java.io.File
import java.io.IOException
Expand Down Expand Up @@ -438,6 +439,18 @@ open class Nimbus(
return nimbusClient.unenrollForGeckoPref(geckoPrefState, prefUnenrollReason)
}

override fun registerPreviousGeckoPrefStates(geckoPrefStates: List<GeckoPrefState>) {
dbScope.launch {
withCatchAll("registerPreviousGeckoPrefStates") {
nimbusClient.registerPreviousGeckoPrefStates(geckoPrefStates)
}
}
}

override fun getPreviousState(experimentSlug: String): PreviousState? {
return nimbusClient.getPreviousState(experimentSlug)
}

@WorkerThread
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun optOutOnThisThread(experimentId: String) = withCatchAll("optOut") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.mozilla.experiments.nimbus.internal.EnrollmentChangeEvent
import org.mozilla.experiments.nimbus.internal.ExperimentBranch
import org.mozilla.experiments.nimbus.internal.GeckoPrefState
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
import org.mozilla.experiments.nimbus.internal.PreviousState
import java.time.Duration
import java.util.concurrent.TimeUnit

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

/**
* Add the original Gecko pref values as a previous state on each involved enrollment.
*
* @param geckoPrefStates The list of items that should have their enrollment state updated with
* original Gecko pref previous state information.
*/
fun registerPreviousGeckoPrefStates(
geckoPrefStates: List<GeckoPrefState>,
) = Unit

/**
* Retrieves a previous state, if available on an enrolled experiment, from a given slug.
*
* @param experimentSlug The slug of the experiment.
* @return The previous state of the given slug. Will return null if not available or invalid slug.
*/
fun getPreviousState(
experimentSlug: String,
): PreviousState? = null

/**
* Reset internal state in response to application-level telemetry reset.
* Consumers should call this method when the user resets the telemetry state of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package org.mozilla.experiments.nimbus

import android.content.Context
import android.os.Looper
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.CancellationException
Expand Down Expand Up @@ -43,11 +44,15 @@ import org.mozilla.experiments.nimbus.internal.GeckoPrefState
import org.mozilla.experiments.nimbus.internal.JsonObject
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.experiments.nimbus.internal.PrefBranch
import org.mozilla.experiments.nimbus.internal.PrefEnrollmentData
import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason
import org.mozilla.experiments.nimbus.internal.PreviousGeckoPrefState
import org.mozilla.experiments.nimbus.internal.PreviousState
import org.mozilla.experiments.nimbus.internal.RecordedContext
import org.mozilla.experiments.nimbus.internal.getCalculatedAttributes
import org.mozilla.experiments.nimbus.internal.validateEventQueries
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import java.io.File
import java.util.Calendar
import java.util.concurrent.Executors
Expand Down Expand Up @@ -849,7 +854,7 @@ class NimbusTests {
"number" to GeckoPrefState(
geckoPref = GeckoPref("pref.number", PrefBranch.DEFAULT),
geckoValue = "1",
enrollmentValue = null,
enrollmentValue = PrefEnrollmentData("test-experiment", "42", "about_welcome", "number"),
isUserSet = false,
),
),
Expand Down Expand Up @@ -911,6 +916,36 @@ class NimbusTests {
assertEquals(EnrollmentChangeEventType.DISQUALIFICATION, events[0].change)
assertEquals(0, handler.setValues?.size)
}

@Test
fun `register previous gecko states and check values`() {
val handler = TestGeckoPrefHandler()

val nimbus = createNimbus(geckoPrefHandler = handler)

suspend fun getString(): String {
return testExperimentsJsonString(appInfo, packageName)
}

val job = nimbus.applyLocalExperiments(::getString)
runBlocking {
job.join()
}

assertEquals(1, handler.setValues?.size)
assertEquals("42", handler.setValues?.get(0)?.enrollmentValue?.prefValue)

nimbus.registerPreviousGeckoPrefStates(handler.setValues!!)
shadowOf(Looper.getMainLooper()).idle()

val previousState = nimbus.getPreviousState("test-experiment")
shadowOf(Looper.getMainLooper()).idle()

assertNotNull(previousState)
val geckoPreviousState = previousState as PreviousState.GeckoPref
assertEquals("1", geckoPreviousState!!.v1.originalValues[0].value)
assertEquals("pref.number", geckoPreviousState!!.v1.originalValues[0].pref)
}
}

// Mocking utilities, from mozilla.components.support.test
Expand Down
52 changes: 45 additions & 7 deletions components/nimbus/src/enrollment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
#[cfg(feature = "stateful")]
use crate::stateful::gecko_prefs::PrefUnenrollReason;
use crate::stateful::gecko_prefs::{OriginalGeckoPref, PrefUnenrollReason};
use crate::{
defaults::Defaults,
error::{debug, warn, NimbusError, Result},
Expand All @@ -21,7 +21,7 @@ pub(crate) const PREVIOUS_ENROLLMENTS_GC_TIME: Duration = Duration::from_secs(36

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

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

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

// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
// The previous state of a Gecko pref before enrollment took place.
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
#[cfg(feature = "stateful")]
pub struct PreviousGeckoPrefState {
pub original_values: Vec<OriginalGeckoPref>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a Vec? Shouldn't it just be an OriginalGeckoPref ?

pub feature_id: String,
pub variable: String,
}

// The previous state of a given feature before enrollment.
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq, uniffi::Enum)]
#[cfg(feature = "stateful")]
pub enum PreviousState {
GeckoPref(PreviousGeckoPrefState),
}

// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ExperimentEnrollment {
pub slug: String,
Expand Down Expand Up @@ -430,6 +449,20 @@ impl ExperimentEnrollment {
}
}

// Previous state is only settable on Enrolled experiments
#[cfg(feature = "stateful")]
pub(crate) fn on_add_state(&self, previous_state: PreviousState) -> ExperimentEnrollment {
let mut next = self.clone();
if let EnrollmentStatus::Enrolled { reason, branch, .. } = &self.status {
next.status = EnrollmentStatus::Enrolled {
previous_state: Some(previous_state),
reason: reason.clone(),
branch: branch.clone(),
};
}
next
}

/// Reset identifiers in response to application-level telemetry reset.
///
/// We move any enrolled experiments to the "disqualified" state, since their further
Expand Down Expand Up @@ -531,12 +564,15 @@ impl ExperimentEnrollment {
}

// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
// ⚠️ in `mod test_schema_bw_compat` below, and may require a DB migration. ⚠️
// ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq, PartialEq)]
pub enum EnrollmentStatus {
Enrolled {
reason: EnrolledReason,
branch: String,
#[cfg(feature = "stateful")]
#[serde(skip_serializing_if = "Option::is_none")]
previous_state: Option<PreviousState>,
},
NotEnrolled {
reason: NotEnrolledReason,
Expand Down Expand Up @@ -577,6 +613,8 @@ impl EnrollmentStatus {
EnrollmentStatus::Enrolled {
reason,
branch: branch.to_owned(),
#[cfg(feature = "stateful")]
previous_state: None,
}
}

Expand Down
8 changes: 8 additions & 0 deletions components/nimbus/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

#[cfg(feature = "stateful")]
use crate::enrollment::PreviousState;

use crate::{enrollment::ExperimentEnrollment, EnrolledFeature, EnrollmentStatus};
use serde_derive::{Deserialize, Serialize};

Expand All @@ -28,6 +31,9 @@ pub struct EnrollmentStatusExtraDef {
pub status: Option<String>,
#[cfg(not(feature = "stateful"))]
pub user_id: Option<String>,
#[cfg(feature = "stateful")]
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_state: Option<PreviousState>,
}

#[cfg(test)]
Expand Down Expand Up @@ -93,6 +99,8 @@ impl From<ExperimentEnrollment> for EnrollmentStatusExtraDef {
status: Some(enrollment.status.name()),
#[cfg(not(feature = "stateful"))]
user_id: None,
#[cfg(feature = "stateful")]
previous_state: None,
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions components/nimbus/src/nimbus.udl
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,23 @@ callback interface MetricsHandler {
void record_malformed_feature_config(MalformedFeatureConfigExtraDef event);
};

typedef enum PreviousState;

dictionary EnrollmentStatusExtraDef {
string? branch;
string? conflict_slug;
string? error_string;
string? reason;
string? slug;
string? status;
PreviousState? previous_state;
};


dictionary PreviousGeckoPrefState {
sequence<OriginalGeckoPref> original_values;
string feature_id;
string variable;
};

dictionary FeatureExposureExtraDef {
Expand Down Expand Up @@ -139,12 +149,20 @@ enum PrefBranch {
"User",
};

dictionary OriginalGeckoPref {
string pref;
PrefBranch branch;
PrefValue? value;
};


enum PrefUnenrollReason {
"Changed",
"FailedToSet",
};

dictionary PrefEnrollmentData {
string experiment_slug;
PrefValue pref_value;
string feature_id;
string variable;
Expand Down Expand Up @@ -362,6 +380,11 @@ interface NimbusClient {
[Throws=NimbusError]
sequence<EnrollmentChangeEvent> unenroll_for_gecko_pref(GeckoPrefState pref_state, PrefUnenrollReason pref_unenroll_reason);

[Throws=NimbusError]
void register_previous_gecko_pref_states([ByRef] sequence<GeckoPrefState> gecko_pref_states);

[Throws=NimbusError]
PreviousState? get_previous_state(string experiment_slug);
};

interface NimbusTargetingHelper {
Expand Down
Loading