Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add enrollments list to targeting attributes for nimbus #5685

Merged
merged 5 commits into from Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,12 @@
- Android: The JVM compatibility target is now version 17 ([#5651](https://github.com/mozilla/application-services/pull/5651))
- _NOTE: This is technically a breaking change, but all existing downstream projects have already made the necessary changes._

## Nimbus ⛅️🔬🔭

### 🦊 What's Changed 🦊

- Add `enrollments` value to `TargetingAttributes` (Nimbus only) — it is a set of strings containing all enrollments, past and present ([#5685](https://github.com/mozilla/application-services/pull/5685)).
jeddai marked this conversation as resolved.
Show resolved Hide resolved

## Nimbus FML ⛅️🔬🔭🔧

### ✨ What's New ✨
Expand Down
2 changes: 1 addition & 1 deletion components/nimbus/src/enrollment.rs
Expand Up @@ -28,7 +28,7 @@ cfg_if::cfg_if! {

#[cfg_attr(not(feature = "stateful"), allow(unused))]
const DEFAULT_GLOBAL_USER_PARTICIPATION: bool = true;
pub(crate) const PREVIOUS_ENROLLMENTS_GC_TIME: Duration = Duration::from_secs(30 * 24 * 3600);
pub(crate) const PREVIOUS_ENROLLMENTS_GC_TIME: Duration = Duration::from_secs(365 * 24 * 3600);
jeddai marked this conversation as resolved.
Show resolved Hide resolved

// These are types we use internally for managing enrollments.
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
Expand Down
1 change: 1 addition & 0 deletions components/nimbus/src/evaluator.rs
Expand Up @@ -43,6 +43,7 @@ pub struct TargetingAttributes {
pub days_since_install: Option<i32>,
pub days_since_update: Option<i32>,
pub active_experiments: HashSet<String>,
pub enrollments: HashSet<String>,
}

#[cfg(not(feature = "stateful"))]
Expand Down
12 changes: 9 additions & 3 deletions components/nimbus/src/nimbus_client.rs
Expand Up @@ -308,14 +308,20 @@ impl NimbusClient {
let enrollments_store = db.get_store(StoreId::Enrollments);
let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;

let mut set = HashSet::<String>::new();
let mut is_enrolled_set = HashSet::<String>::new();
let mut all_enrolled_set = HashSet::<String>::new();
for ee in prev_enrollments {
if let EnrollmentStatus::Enrolled { .. } = ee.status {
set.insert(ee.slug.clone());
is_enrolled_set.insert(ee.slug.clone());
all_enrolled_set.insert(ee.slug.clone());
}
if let EnrollmentStatus::WasEnrolled { .. } = ee.status {
all_enrolled_set.insert(ee.slug.clone());
jeddai marked this conversation as resolved.
Show resolved Hide resolved
}
}

state.targeting_attributes.active_experiments = set;
state.targeting_attributes.active_experiments = is_enrolled_set;
state.targeting_attributes.enrollments = all_enrolled_set;

Ok(())
}
Expand Down
272 changes: 272 additions & 0 deletions components/nimbus/src/tests/stateful/test_nimbus.rs
Expand Up @@ -971,3 +971,275 @@ fn test_fetch_enabled() -> Result<()> {
assert!(!client.is_fetch_enabled()?);
Ok(())
}

#[test]
fn test_active_enrollment_in_targeting() -> Result<()> {
let mock_client_id = "client-1".to_string();

let temp_dir = tempfile::tempdir()?;

let app_context = AppContext {
app_name: "fenix".to_string(),
app_id: "org.mozilla.fenix".to_string(),
channel: "nightly".to_string(),
..Default::default()
};
let mut client = NimbusClient::new(
app_context.clone(),
temp_dir.path(),
None,
AvailableRandomizationUnits {
client_id: Some(mock_client_id),
..AvailableRandomizationUnits::default()
},
)?;
let targeting_attributes = TargetingAttributes {
app_context,
..Default::default()
};
client.with_targeting_attributes(targeting_attributes);
client.initialize()?;

// Apply an initial experiment
let experiment_json = serde_json::to_string(&json!({
"data": [{
"schemaVersion": "1.0.0",
"slug": "test-1",
"endDate": null,
"featureIds": ["some-feature-1"],
"branches": [
{
"slug": "control",
"ratio": 1
},
{
"slug": "treatment",
"ratio": 1
}
],
"channel": "nightly",
"probeSets": [],
"startDate": null,
"appName": "fenix",
"appId": "org.mozilla.fenix",
"bucketConfig": {
"count": 10000,
"start": 0,
"total": 10000,
"namespace": "secure-gold",
"randomizationUnit": "nimbus_id"
},
"targeting": "true",
"userFacingName": "test experiment",
"referenceBranch": "control",
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "This is a test experiment for testing purposes.",
"id": "secure-copper",
"last_modified": 1_602_197_324_372i64,
}
]}))?;
jeddai marked this conversation as resolved.
Show resolved Hide resolved
client.set_experiments_locally(experiment_json)?;
client.apply_pending_experiments()?;

let active_experiments = client.get_active_experiments()?;
assert_eq!(active_experiments.len(), 1);

let targeting_helper = client.create_targeting_helper(None)?;
assert!(targeting_helper.eval_jexl("'test-1' in active_experiments".to_string())?);

// Apply experiment that targets the above experiment is in enrollments
let experiment_json = serde_json::to_string(&json!({
"data": [{
"schemaVersion": "1.0.0",
"slug": "test-2",
"endDate": null,
"featureIds": ["some-feature-1"],
"branches": [
{
"slug": "control",
"ratio": 1
},
{
"slug": "treatment",
"ratio": 1
}
],
"channel": "nightly",
"probeSets": [],
"startDate": null,
"appName": "fenix",
"appId": "org.mozilla.fenix",
"bucketConfig": {
"count": 10000,
"start": 0,
"total": 10000,
"namespace": "secure-gold",
"randomizationUnit": "nimbus_id"
},
"targeting": "'test-1' in enrollments",
"userFacingName": "test experiment",
"referenceBranch": "control",
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "This is a test experiment for testing purposes.",
"id": "secure-copper",
"last_modified": 1_602_197_324_372i64,
}
]}))?;
client.set_experiments_locally(experiment_json)?;
client.apply_pending_experiments()?;

let active_experiments = client.get_active_experiments()?;
assert_eq!(active_experiments.len(), 1);

let targeting_helper = client.create_targeting_helper(None)?;
assert!(!targeting_helper.eval_jexl("'test-1' in active_experiments".to_string())?);
assert!(targeting_helper.eval_jexl("'test-2' in active_experiments".to_string())?);
assert!(targeting_helper.eval_jexl("'test-1' in enrollments".to_string())?);
jeddai marked this conversation as resolved.
Show resolved Hide resolved
assert!(targeting_helper.eval_jexl("'test-2' in enrollments".to_string())?);

Ok(())
}

#[test]
fn test_previous_enrollment_in_targeting() -> Result<()> {
let mock_client_id = "client-1".to_string();

let temp_dir = tempfile::tempdir()?;

let app_context = AppContext {
app_name: "fenix".to_string(),
app_id: "org.mozilla.fenix".to_string(),
channel: "nightly".to_string(),
..Default::default()
};
let mut client = NimbusClient::new(
app_context.clone(),
temp_dir.path(),
None,
AvailableRandomizationUnits {
client_id: Some(mock_client_id),
..AvailableRandomizationUnits::default()
},
)?;
let targeting_attributes = TargetingAttributes {
app_context,
..Default::default()
};
client.with_targeting_attributes(targeting_attributes);
client.initialize()?;

// Apply an initial experiment
let experiment_json = serde_json::to_string(&json!({
"data": [{
"schemaVersion": "1.0.0",
"slug": "test-1",
"endDate": null,
"featureIds": ["some-feature-1"],
"branches": [
{
"slug": "control",
"ratio": 1
},
{
"slug": "treatment",
"ratio": 1
}
],
"channel": "nightly",
"probeSets": [],
"startDate": null,
"appName": "fenix",
"appId": "org.mozilla.fenix",
"bucketConfig": {
"count": 10000,
"start": 0,
"total": 10000,
"namespace": "secure-gold",
"randomizationUnit": "nimbus_id"
},
"targeting": "true",
"userFacingName": "test experiment",
"referenceBranch": "control",
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "This is a test experiment for testing purposes.",
"id": "secure-copper",
"last_modified": 1_602_197_324_372i64,
}
]}))?;
client.set_experiments_locally(experiment_json)?;
client.apply_pending_experiments()?;

let active_experiments = client.get_active_experiments()?;
assert_eq!(active_experiments.len(), 1);

let targeting_helper = client.create_targeting_helper(None)?;
assert!(targeting_helper.eval_jexl("'test-1' in active_experiments".to_string())?);

// Apply empty experiment list
let experiment_json = serde_json::to_string(&json!({"data": []}))?;
client.set_experiments_locally(experiment_json)?;
client.apply_pending_experiments()?;

let active_experiments = client.get_active_experiments()?;
assert_eq!(active_experiments.len(), 0);

let targeting_helper = client.create_targeting_helper(None)?;
assert!(!targeting_helper.eval_jexl("'test-1' in active_experiments".into())?);
assert!(targeting_helper.eval_jexl("'test-1' in enrollments".into())?);

// Apply experiment that targets the above experiment is in enrollments
let experiment_json = serde_json::to_string(&json!({
"data": [{
"schemaVersion": "1.0.0",
"slug": "test-2",
"endDate": null,
"featureIds": ["some-feature-1"],
"branches": [
{
"slug": "control",
"ratio": 1
},
{
"slug": "treatment",
"ratio": 1
}
],
"channel": "nightly",
"probeSets": [],
"startDate": null,
"appName": "fenix",
"appId": "org.mozilla.fenix",
"bucketConfig": {
"count": 10000,
"start": 0,
"total": 10000,
"namespace": "secure-gold",
"randomizationUnit": "nimbus_id"
},
"targeting": "'test-1' in enrollments",
"userFacingName": "test experiment",
"referenceBranch": "control",
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "This is a test experiment for testing purposes.",
"id": "secure-copper",
"last_modified": 1_602_197_324_372i64,
}
]}))?;
client.set_experiments_locally(experiment_json)?;
client.apply_pending_experiments()?;

let active_experiments = client.get_active_experiments()?;
assert_eq!(active_experiments.len(), 1);

let targeting_helper = client.create_targeting_helper(None)?;
assert!(!targeting_helper.eval_jexl("'test-1' in active_experiments".to_string())?);
assert!(targeting_helper.eval_jexl("'test-2' in active_experiments".to_string())?);
assert!(targeting_helper.eval_jexl("'test-1' in enrollments".to_string())?);
assert!(targeting_helper.eval_jexl("'test-2' in enrollments".to_string())?);

Ok(())
}