Skip to content

Commit e9e3d51

Browse files
committed
add enrollment_status metric
update changelog
1 parent 8a74932 commit e9e3d51

File tree

24 files changed

+560
-25
lines changed

24 files changed

+560
-25
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
[Full Changelog](In progress)
44

5+
## Nimbus SDK ⛅️🔬🔭
6+
7+
### ✨ What's New ✨
8+
9+
- Added the `enrollment_status` metric and defined a host metric callback handler interface ([#5857](https://github.com/mozilla/application-services/pull/5857)).
10+
511
## Rust log forwarder
612

713
### 🦊 What's Changed 🦊

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import org.mozilla.experiments.nimbus.internal.AvailableRandomizationUnits
3333
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
3434
import org.mozilla.experiments.nimbus.internal.EnrollmentChangeEvent
3535
import org.mozilla.experiments.nimbus.internal.EnrollmentChangeEventType
36+
import org.mozilla.experiments.nimbus.internal.EnrollmentStatusExtraDef
37+
import org.mozilla.experiments.nimbus.internal.MetricsHandler
3638
import org.mozilla.experiments.nimbus.internal.NimbusClient
3739
import org.mozilla.experiments.nimbus.internal.NimbusClientInterface
3840
import org.mozilla.experiments.nimbus.internal.NimbusException
@@ -77,6 +79,23 @@ open class Nimbus(
7779

7880
private val logger = delegate.logger
7981

82+
private val metricsHandler = object : MetricsHandler {
83+
override fun recordEnrollmentStatuses(enrollmentStatusExtras: List<EnrollmentStatusExtraDef>) {
84+
for (extra in enrollmentStatusExtras) {
85+
NimbusEvents.enrollmentStatus.record(
86+
NimbusEvents.EnrollmentStatusExtra(
87+
branch = extra.branch,
88+
slug = extra.slug,
89+
status = extra.status,
90+
reason = extra.reason,
91+
errorString = extra.errorString,
92+
conflictSlug = extra.conflictSlug,
93+
),
94+
)
95+
}
96+
}
97+
}
98+
8099
private val nimbusClient: NimbusClientInterface
81100

82101
override var globalUserParticipation: Boolean
@@ -118,6 +137,7 @@ open class Nimbus(
118137
// The "dummy" field here is required for obscure reasons when generating code on desktop,
119138
// so we just automatically set it to a dummy value.
120139
AvailableRandomizationUnits(clientId = null, userId = null, nimbusId = null, dummy = 0),
140+
metricsHandler,
121141
)
122142
}
123143

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,31 @@ class NimbusTests {
644644

645645
assertTrue(observed)
646646
}
647+
648+
@Test
649+
fun `Nimbus records EnrollmentStatus metrics`() {
650+
suspend fun getString(): String {
651+
return testExperimentsJsonString(appInfo, packageName)
652+
}
653+
654+
val job = nimbus.applyLocalExperiments(::getString)
655+
runBlocking {
656+
job.join()
657+
}
658+
659+
assertEquals(1, nimbus.getAvailableExperiments().size)
660+
assertNotNull("Event must have a value", NimbusEvents.enrollmentStatus.testGetValue())
661+
val enrollmentStatusEvents = NimbusEvents.enrollmentStatus.testGetValue()!!
662+
assertEquals("Event count must match", enrollmentStatusEvents.count(), 1)
663+
664+
val enrolledExtra = enrollmentStatusEvents[0].extra!!
665+
assertEquals("branch must match", "test-branch", enrolledExtra["branch"])
666+
assertEquals("slug must match", "test-experiment", enrolledExtra["slug"])
667+
assertEquals("status must match", "Enrolled", enrolledExtra["status"])
668+
assertEquals("reason must match", "Qualified", enrolledExtra["reason"])
669+
assertEquals("errorString must match", null, enrolledExtra["error_string"])
670+
assertEquals("conflictSlug must match", null, enrolledExtra["conflict_slug"])
671+
}
647672
}
648673

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

components/nimbus/examples/experiment.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ fn main() -> Result<()> {
1212
use clap::{App, Arg, SubCommand};
1313
use env_logger::Env;
1414
use nimbus::{
15+
metrics::{EnrollmentStatusExtraDef, MetricsHandler},
1516
AppContext, AvailableRandomizationUnits, EnrollmentStatus, NimbusClient,
1617
NimbusTargetingHelper, RemoteSettingsConfig,
1718
};
1819
use std::collections::HashMap;
1920
use std::io::prelude::*;
2021

22+
pub struct NoopMetricsHandler;
23+
24+
impl MetricsHandler for NoopMetricsHandler {
25+
fn record_enrollment_statuses(&self, _: Vec<EnrollmentStatusExtraDef>) {
26+
// do nothing
27+
}
28+
}
29+
2130
// We set the logging level to be `warn` here, meaning that only
2231
// logs of `warn` or higher will be actually be shown, any other
2332
// error will be omitted
@@ -208,6 +217,7 @@ fn main() -> Result<()> {
208217
db_path,
209218
Some(config),
210219
aru,
220+
Box::new(NoopMetricsHandler),
211221
)?;
212222
log::info!("Nimbus ID is {}", nimbus_client.nimbus_id()?);
213223

components/nimbus/ios/Nimbus/NimbusCreate.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import Foundation
6+
import Glean
67
import UIKit
78

89
private let logTag = "Nimbus.swift"
@@ -18,6 +19,22 @@ public let defaultErrorReporter: NimbusErrorReporter = { err in
1819
}
1920
}
2021

22+
class GleanMetricsHandler: MetricsHandler {
23+
func recordEnrollmentStatuses(enrollmentStatusExtras: [EnrollmentStatusExtraDef]) {
24+
for extra in enrollmentStatusExtras {
25+
GleanMetrics.NimbusEvents.enrollmentStatus
26+
.record(GleanMetrics.NimbusEvents.EnrollmentStatusExtra(
27+
branch: extra.branch,
28+
conflictSlug: extra.conflictSlug,
29+
errorString: extra.errorString,
30+
reason: extra.reason,
31+
slug: extra.slug,
32+
status: extra.status
33+
))
34+
}
35+
}
36+
}
37+
2138
public extension Nimbus {
2239
/// Create an instance of `Nimbus`.
2340
///
@@ -63,7 +80,8 @@ public extension Nimbus {
6380
userId: nil,
6481
nimbusId: nil,
6582
dummy: 0
66-
)
83+
),
84+
metricsHandler: GleanMetricsHandler()
6785
)
6886

6987
return Nimbus(nimbusClient: nimbusClient, resourceBundles: resourceBundles, errorReporter: errorReporter)

components/nimbus/metrics.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,39 @@ nimbus_events:
203203
- jhugman@mozilla.com
204204
- nimbus-team@mozilla.com
205205
expires: never
206+
enrollment_status:
207+
type: event
208+
description: >
209+
Recorded for each enrollment status each time the SDK completes application of pending experiments.
210+
extra_keys:
211+
slug:
212+
type: string
213+
description: The slug/unique identifier of the experiment
214+
status:
215+
type: string
216+
description: The status of this enrollment
217+
reason:
218+
type: string
219+
description: The reason the client is in the noted status
220+
branch:
221+
type: string
222+
description: The branch slug/identifier that was randomly chosen (if the client is enrolled)
223+
error_string:
224+
type: string
225+
description: If the enrollment resulted in an error, the associated error string
226+
conflict_slug:
227+
type: string
228+
description: If the enrollment hit a feature conflict, the slug of the conflicting experiment/rollout
229+
bugs:
230+
- https://mozilla-hub.atlassian.net/browse/EXP-3827
231+
data_reviews:
232+
- ''
233+
data_sensitivity:
234+
- technical
235+
notification_emails:
236+
- chumphreys@mozilla.com
237+
- project-nimbus@mozilla.com
238+
expires: never
206239
nimbus_health:
207240
cache_not_ready_for_feature:
208241
type: event

components/nimbus/src/enrollment.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use ::uuid::Uuid;
1212
use serde_derive::*;
1313
use std::{
1414
collections::{HashMap, HashSet},
15+
fmt::{Display, Formatter, Result as FmtResult},
1516
time::{Duration, SystemTime, UNIX_EPOCH},
1617
};
1718

@@ -28,6 +29,18 @@ pub enum EnrolledReason {
2829
OptIn,
2930
}
3031

32+
impl Display for EnrolledReason {
33+
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
34+
Display::fmt(
35+
match self {
36+
EnrolledReason::Qualified => "Qualified",
37+
EnrolledReason::OptIn => "OptIn",
38+
},
39+
f,
40+
)
41+
}
42+
}
43+
3144
// These are types we use internally for managing non-enrollments.
3245

3346
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
@@ -46,6 +59,21 @@ pub enum NotEnrolledReason {
4659
FeatureConflict,
4760
}
4861

62+
impl Display for NotEnrolledReason {
63+
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
64+
Display::fmt(
65+
match self {
66+
NotEnrolledReason::OptOut => "OptOut",
67+
NotEnrolledReason::NotSelected => "NotSelected",
68+
NotEnrolledReason::NotTargeted => "NotTargeted",
69+
NotEnrolledReason::EnrollmentsPaused => "EnrollmentsPaused",
70+
NotEnrolledReason::FeatureConflict => "FeatureConflict",
71+
},
72+
f,
73+
)
74+
}
75+
}
76+
4977
// These are types we use internally for managing disqualifications.
5078

5179
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
@@ -62,6 +90,20 @@ pub enum DisqualifiedReason {
6290
NotSelected,
6391
}
6492

93+
impl Display for DisqualifiedReason {
94+
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
95+
Display::fmt(
96+
match self {
97+
DisqualifiedReason::Error => "Error",
98+
DisqualifiedReason::OptOut => "OptOut",
99+
DisqualifiedReason::NotSelected => "NotSelected",
100+
DisqualifiedReason::NotTargeted => "NotTargeted",
101+
},
102+
f,
103+
)
104+
}
105+
}
106+
65107
// Every experiment has an ExperimentEnrollment, even when we aren't enrolled.
66108

67109
// ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️
@@ -439,7 +481,9 @@ impl ExperimentEnrollment {
439481
},
440482
EnrollmentChangeEventType::Disqualification,
441483
),
442-
EnrollmentStatus::NotEnrolled { .. } | EnrollmentStatus::Error { .. } => unreachable!(),
484+
EnrollmentStatus::NotEnrolled { .. } | EnrollmentStatus::Error { .. } => {
485+
unreachable!()
486+
}
443487
}
444488
}
445489

@@ -496,6 +540,19 @@ pub enum EnrollmentStatus {
496540
},
497541
}
498542

543+
impl EnrollmentStatus {
544+
pub fn name(&self) -> String {
545+
match self {
546+
EnrollmentStatus::Enrolled { .. } => "Enrolled",
547+
EnrollmentStatus::NotEnrolled { .. } => "NotEnrolled",
548+
EnrollmentStatus::Disqualified { .. } => "Disqualified",
549+
EnrollmentStatus::WasEnrolled { .. } => "WasEnrolled",
550+
EnrollmentStatus::Error { .. } => "Error",
551+
}
552+
.into()
553+
}
554+
}
555+
499556
impl EnrollmentStatus {
500557
// Note that for now, we only support a single feature_id per experiment,
501558
// so this code is expected to shift once we start supporting multiple.

components/nimbus/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ pub enum NimbusError {
6666
#[cfg(not(feature = "stateful"))]
6767
#[error("Error in Cirrus: {0}")]
6868
CirrusError(#[from] CirrusClientError),
69+
#[cfg(feature = "stateful")]
70+
#[error("UniFFI callback error: {0}")]
71+
UniFFICallbackError(#[from] uniffi::UnexpectedUniFFICallbackError),
6972
}
7073

7174
#[cfg(feature = "stateful")]

components/nimbus/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
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+
extern crate core;
6+
57
mod defaults;
68
mod enrollment;
79
mod evaluator;
@@ -23,6 +25,8 @@ pub use targeting::NimbusTargetingHelper;
2325

2426
cfg_if::cfg_if! {
2527
if #[cfg(feature = "stateful")] {
28+
pub mod metrics;
29+
2630
pub mod stateful;
2731

2832
pub use stateful::nimbus_client::*;

0 commit comments

Comments
 (0)