use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use crate::error_recording::{record_error, ErrorType};
use crate::metrics::Metric;
use crate::metrics::MetricType;
use crate::storage::StorageManager;
use crate::util::{truncate_string_at_boundary, truncate_string_at_boundary_with_error};
use crate::CommonMetricData;
use crate::Glean;
use crate::Lifetime;
const INTERNAL_STORAGE: &str = "glean_internal_info";
const MAX_EXPERIMENTS_IDS_LEN: usize = 100;
const MAX_EXPERIMENT_VALUE_LEN: usize = MAX_EXPERIMENTS_IDS_LEN;
const MAX_EXPERIMENTS_EXTRAS_SIZE: usize = 20;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct RecordedExperimentData {
pub branch: String,
pub extra: Option<HashMap<String, String>>,
}
#[derive(Clone, Debug)]
pub struct ExperimentMetric {
meta: CommonMetricData,
}
impl MetricType for ExperimentMetric {
fn meta(&self) -> &CommonMetricData {
&self.meta
}
fn meta_mut(&mut self) -> &mut CommonMetricData {
&mut self.meta
}
}
impl ExperimentMetric {
pub fn new(glean: &Glean, id: String) -> Self {
let mut error = None;
let truncated_id = if id.len() > MAX_EXPERIMENTS_IDS_LEN {
let msg = format!(
"Value length {} for experiment id exceeds maximum of {}",
id.len(),
MAX_EXPERIMENTS_IDS_LEN
);
error = Some(msg);
truncate_string_at_boundary(id, MAX_EXPERIMENTS_IDS_LEN)
} else {
id
};
let new_experiment = Self {
meta: CommonMetricData {
name: format!("{}#experiment", truncated_id),
category: "".into(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::Application,
..Default::default()
},
};
if let Some(msg) = error {
record_error(
glean,
&new_experiment.meta,
ErrorType::InvalidValue,
msg,
None,
);
}
new_experiment
}
pub fn set_active(
&self,
glean: &Glean,
branch: String,
extra: Option<HashMap<String, String>>,
) {
if !self.should_record(glean) {
return;
}
let truncated_branch = if branch.len() > MAX_EXPERIMENTS_IDS_LEN {
truncate_string_at_boundary_with_error(
glean,
&self.meta,
branch,
MAX_EXPERIMENTS_IDS_LEN,
)
} else {
branch
};
let truncated_extras = extra.and_then(|extra| {
if extra.len() > MAX_EXPERIMENTS_EXTRAS_SIZE {
let msg = format!(
"Extra hash map length {} exceeds maximum of {}",
extra.len(),
MAX_EXPERIMENTS_EXTRAS_SIZE
);
record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
}
let mut temp_map = HashMap::new();
for (key, value) in extra.into_iter().take(MAX_EXPERIMENTS_EXTRAS_SIZE) {
let truncated_key = if key.len() > MAX_EXPERIMENTS_IDS_LEN {
truncate_string_at_boundary_with_error(
glean,
&self.meta,
key,
MAX_EXPERIMENTS_IDS_LEN,
)
} else {
key
};
let truncated_value = if value.len() > MAX_EXPERIMENT_VALUE_LEN {
truncate_string_at_boundary_with_error(
glean,
&self.meta,
value,
MAX_EXPERIMENT_VALUE_LEN,
)
} else {
value
};
temp_map.insert(truncated_key, truncated_value);
}
Some(temp_map)
});
let value = Metric::Experiment(RecordedExperimentData {
branch: truncated_branch,
extra: truncated_extras,
});
glean.storage().record(glean, &self.meta, &value)
}
pub fn set_inactive(&self, glean: &Glean) {
if !self.should_record(glean) {
return;
}
if let Err(e) = glean.storage().remove_single_metric(
Lifetime::Application,
INTERNAL_STORAGE,
&self.meta.name,
) {
log::error!("Failed to set experiment as inactive: {:?}", e);
}
}
pub fn test_get_value_as_json_string(&self, glean: &Glean) -> Option<String> {
match StorageManager.snapshot_metric(
glean.storage(),
INTERNAL_STORAGE,
&self.meta.identifier(glean),
) {
Some(Metric::Experiment(e)) => Some(json!(e).to_string()),
_ => None,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn stable_serialization() {
let experiment_empty = RecordedExperimentData {
branch: "branch".into(),
extra: None,
};
let mut data = HashMap::new();
data.insert("a key".to_string(), "a value".to_string());
let experiment_data = RecordedExperimentData {
branch: "branch".into(),
extra: Some(data),
};
let experiment_empty_bin = bincode::serialize(&experiment_empty).unwrap();
let experiment_data_bin = bincode::serialize(&experiment_data).unwrap();
assert_eq!(
experiment_empty,
bincode::deserialize(&experiment_empty_bin).unwrap()
);
assert_eq!(
experiment_data,
bincode::deserialize(&experiment_data_bin).unwrap()
);
}
#[test]
#[rustfmt::skip] fn deserialize_old_encoding() {
let empty_bin = vec![6, 0, 0, 0, 0, 0, 0, 0, 98, 114, 97, 110, 99, 104];
let data_bin = vec![6, 0, 0, 0, 0, 0, 0, 0, 98, 114, 97, 110, 99, 104,
1, 1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0,
97, 32, 107, 101, 121, 7, 0, 0, 0, 0, 0, 0, 0, 97,
32, 118, 97, 108, 117, 101];
let mut data = HashMap::new();
data.insert("a key".to_string(), "a value".to_string());
let experiment_data = RecordedExperimentData { branch: "branch".into(), extra: Some(data), };
let experiment_empty: Result<RecordedExperimentData, _> = bincode::deserialize(&empty_bin);
assert!(experiment_empty.is_err());
assert_eq!(experiment_data, bincode::deserialize(&data_bin).unwrap());
}
}