/* This Source Code Form is subject to the terms of the Mozilla Public * 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/. */ use super::glean_metrics; use anyhow::Context; use glean::private::{ BooleanMetric, DatetimeMetric, ObjectMetric, QuantityMetric, StringListMetric, StringMetric, TimespanMetric, }; use glean::TestGetValue; pub struct Annotation { pub key: &'static str, pub glean_key: &'static str, #[cfg(test)] pub convert_fn: &'static str, pub set_glean_metric: fn(&serde_json::Value) -> anyhow::Result<()>, pub test_get_glean_value: fn() -> Option, } macro_rules! convert { ( $category:ident :: $metric:ident = $func:ident ($key:literal $(, $($args:expr),*)? ) ) => { Annotation { key: $key, glean_key: concat!(stringify!($category), ".", stringify!($metric)), #[cfg(test)] convert_fn: stringify!($func), set_glean_metric: |value: &serde_json::Value| -> anyhow::Result<()> { SourceValue(value).try_into().map_err(|e| anyhow::Error::from(e)) .and_then(|v| $func(&glean_metrics::$category::$metric, v $(, $($args),*)? )) .context(concat!("while trying to set ", stringify!($category), "::", stringify!($metric), " from annotation ", $key)) }, test_get_glean_value: || -> Option { glean_metrics::$category::$metric.test_get_value(Some("crash".into())) .map(|v| TestGetValueToJson(v).into()) } } }; } // Env variable set to the file generated by conversions.py (by build.rs). include!(env!("CONVERSIONS_FILE")); struct SourceValue<'a>(&'a serde_json::Value); impl<'a> TryFrom> for &'a str { type Error = anyhow::Error; fn try_from(value: SourceValue<'a>) -> Result { value.0.as_str().context("expected a string") } } impl<'a> TryFrom> for &'a serde_json::Value { type Error = std::convert::Infallible; fn try_from(value: SourceValue<'a>) -> Result { Ok(value.0) } } struct TestGetValueToJson(T); macro_rules! TestGetValueToJson_passthrough { ( $($T:ty),* ) => { $( impl From> for serde_json::Value { fn from(value: TestGetValueToJson<$T>) -> Self { value.0.into() } })* } } TestGetValueToJson_passthrough![serde_json::Value, String, bool, i64, Vec]; impl From> for serde_json::Value { fn from(value: TestGetValueToJson) -> Self { let glean::Datetime { year, month, day, hour, minute, second, nanosecond, offset_seconds, } = value.0; time::OffsetDateTime::new_in_offset( time::Date::from_calendar_date(year, (month as u8).try_into().unwrap(), day as u8) .unwrap(), time::Time::from_hms_nano(hour as u8, minute as u8, second as u8, nanosecond).unwrap(), time::UtcOffset::from_whole_seconds(offset_seconds).unwrap(), ) .to_string() .into() } } fn convert_boolean_to_boolean(metric: &BooleanMetric, value: &str) -> anyhow::Result<()> { metric.set(value == "1"); Ok(()) } fn convert_string_to_string(metric: &StringMetric, value: &str) -> anyhow::Result<()> { metric.set(value.to_owned()); Ok(()) } fn convert_u64_to_quantity(metric: &QuantityMetric, value: &str) -> anyhow::Result<()> { metric.set(value.parse().context("couldn't parse quantity")?); Ok(()) } fn convert_string_to_string_list( metric: &StringListMetric, value: &str, delimiter: &str, ) -> anyhow::Result<()> { metric.set( value .split(delimiter) .filter(|s| !s.is_empty()) .map(|s| s.to_owned()) .collect(), ); Ok(()) } fn convert_to_crash_time(metric: &DatetimeMetric, value: &str) -> anyhow::Result<()> { let seconds_since_epoch: i64 = value .parse() .context("failed to parse crash time seconds")?; metric.set(Some(glean_datetime( time::OffsetDateTime::from_unix_timestamp(seconds_since_epoch) .context("failed to convert crash time unix timestamp")?, ))); Ok(()) } fn convert_duration_seconds(metric: &TimespanMetric, value: &str) -> anyhow::Result<()> { metric.set_raw(std::time::Duration::from_secs( value .parse() .context("failed to parse seconds since last crash")?, )); Ok(()) } fn convert_to_environment_uptime(metric: &TimespanMetric, value: &str) -> anyhow::Result<()> { metric.set_raw(std::time::Duration::from_secs_f32( value.parse().context("failed to parse uptime")?, )); Ok(()) } fn convert_to_crash_windows_file_dialog_error_code( metric: &StringMetric, value: &str, ) -> anyhow::Result<()> { metric.set(value.to_owned()); // Just assume it is an integer as expected Ok(()) } fn convert_to_crash_async_shutdown_timeout( metric: &ObjectMetric, value: &str, ) -> anyhow::Result<()> { // The JSON value is stored as a string, so we need to deserialize it. let mut value: serde_json::Map = serde_json::from_str(value)?; metric.set(glean_metrics::crash::AsyncShutdownTimeoutObject { broken_add_blockers: { if let Some(broken_add_blockers) = value.remove("brokenAddBlockers") { let broken_add_blockers = broken_add_blockers .as_array() .context("expected array for brokenAddBlockers")?; let mut ret = Vec::with_capacity(broken_add_blockers.len()); for entry in broken_add_blockers { ret.push( entry .as_str() .context("expected brokenAddBlockers entry to be a string")? .to_owned(), ); } ret } else { Default::default() } }, conditions: { if let Some(conditions) = value.remove("conditions") { Some(if let Some(s) = conditions.as_str() { s.to_owned() } else { serde_json::to_string(&conditions).context("failed to serialize conditions")? }) } else { None } }, phase: { if let Some(phase) = value.remove("phase") { Some( phase .as_str() .context("expected phase to be a string")? .to_owned(), ) } else { None } }, }); Ok(()) } fn convert_to_crash_quota_manager_shutdown_timeout( metric: &ObjectMetric, value: &str, ) -> anyhow::Result<()> { // The Glean metric is an array of the lines. metric.set(value.lines().map(|s| s.to_owned()).collect::>()); Ok(()) } fn convert_to_crash_stack_traces( metric: &ObjectMetric, value: &serde_json::Value, ) -> anyhow::Result<()> { let error = value["status"] .as_str() .and_then(|v| (v != "OK").then_some(v.to_owned())); let crash_type = value["crash_info"]["type"].as_str().map(|s| s.to_owned()); let crash_address = value["crash_info"]["address"] .as_str() .map(|s| s.to_owned()); let crash_thread = value["crash_info"]["crashing_thread"].as_i64(); let main_module = value["main_module"].as_i64(); let modules = value["modules"] .as_array() .map(|modules| { modules .iter() .map(|m| glean_metrics::crash::StackTracesObjectModulesItem { base_address: m["base_addr"].as_str().map(|s| s.to_owned()), end_address: m["end_addr"].as_str().map(|s| s.to_owned()), code_id: m["code_id"].as_str().map(|s| s.to_owned()), debug_file: m["debug_file"].as_str().map(|s| s.to_owned()), debug_id: m["debug_id"].as_str().map(|s| s.to_owned()), filename: m["filename"].as_str().map(|s| s.to_owned()), version: m["version"].as_str().map(|s| s.to_owned()), }) .collect::>() }) .unwrap_or_default(); let threads = value["threads"] .as_array() .map(|threads| { threads .iter() .map(|t| glean_metrics::crash::StackTracesObjectThreadsItem { frames: t["frames"] .as_array() .map(|frames| { frames .iter() .map(|f| { glean_metrics::crash::StackTracesObjectThreadsItemFramesItem { module_index: f["module_index"].as_i64(), ip: f["ip"].as_str().map(|s| s.to_owned()), trust: f["trust"].as_str().map(|s| s.to_owned()), } }) .collect::>() }) .unwrap_or_default(), }) .collect::>() }) .unwrap_or_default(); metric.set(glean_metrics::crash::StackTracesObject { error, crash_type, crash_address, crash_thread, main_module, modules, threads, }); Ok(()) } // The format of the JSON value is defined in getStacktraceAsJsonString // (mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/Throwable.kt). fn convert_to_crash_java_exception( metric: &ObjectMetric, value: &str, ) -> anyhow::Result<()> { // The JSON value is stored as a string, so we need to deserialize it. let value: serde_json::Map = serde_json::from_str(value)?; let throwables = value["exception"]["values"] .as_array() .context("expected throwables array")? .iter() // The format stores throwables in reverse order (deepest cause first), but we define the // glean format to be by top cause first. .rev() .map(|throwable| &throwable["stacktrace"]) .map( |throwable| glean_metrics::crash::JavaExceptionObjectThrowablesItem { message: throwable["value"].as_str().map(ToOwned::to_owned), type_name: throwable["module"] .as_str() .zip(throwable["type"].as_str()) .map(|(m, t)| format!("{m}.{t}")), stack: throwable["frames"] .as_array() .map(|frames| { frames .iter() .map(|frame| { glean_metrics::crash::JavaExceptionObjectThrowablesItemStackItem { class_name: frame["module"].as_str().map(ToOwned::to_owned), method_name: frame["function"].as_str().map(ToOwned::to_owned), is_native: frame["in_app"].as_bool().map(|b| !b), line: frame["lineno"].as_i64(), file: frame["filename"].as_str().map(ToOwned::to_owned), } }) .collect() }) .unwrap_or_default(), }, ) .collect(); metric.set(glean_metrics::crash::JavaExceptionObject { throwables }); Ok(()) } fn glean_datetime(datetime: time::OffsetDateTime) -> glean::Datetime { glean::Datetime { year: datetime.year(), month: datetime.month() as _, day: datetime.day() as _, hour: datetime.hour() as _, minute: datetime.minute() as _, second: datetime.second() as _, nanosecond: datetime.nanosecond(), offset_seconds: datetime.offset().whole_seconds(), } }