Source code

Revision control

Copy as Markdown

Other Tools

/* 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<serde_json::Value>,
}
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<serde_json::Value> {
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<SourceValue<'a>> for &'a str {
type Error = anyhow::Error;
fn try_from(value: SourceValue<'a>) -> Result<Self, Self::Error> {
value.0.as_str().context("expected a string")
}
}
impl<'a> TryFrom<SourceValue<'a>> for &'a serde_json::Value {
type Error = std::convert::Infallible;
fn try_from(value: SourceValue<'a>) -> Result<Self, Self::Error> {
Ok(value.0)
}
}
struct TestGetValueToJson<T>(T);
macro_rules! TestGetValueToJson_passthrough {
( $($T:ty),* ) => {
$(
impl From<TestGetValueToJson<$T>> for serde_json::Value {
fn from(value: TestGetValueToJson<$T>) -> Self {
value.0.into()
}
})*
}
}
TestGetValueToJson_passthrough![serde_json::Value, String, bool, i64, Vec<String>];
impl From<TestGetValueToJson<glean::Datetime>> for serde_json::Value {
fn from(value: TestGetValueToJson<glean::Datetime>) -> 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<glean_metrics::crash::AsyncShutdownTimeoutObject>,
value: &str,
) -> anyhow::Result<()> {
// The JSON value is stored as a string, so we need to deserialize it.
let mut value: serde_json::Map<String, serde_json::Value> = 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<glean_metrics::crash::QuotaManagerShutdownTimeoutObject>,
value: &str,
) -> anyhow::Result<()> {
// The Glean metric is an array of the lines.
metric.set(value.lines().map(|s| s.to_owned()).collect::<Vec<_>>());
Ok(())
}
fn convert_to_crash_stack_traces(
metric: &ObjectMetric<glean_metrics::crash::StackTracesObject>,
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::<Vec<_>>()
})
.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::<Vec<_>>()
})
.unwrap_or_default(),
})
.collect::<Vec<_>>()
})
.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<glean_metrics::crash::JavaExceptionObject>,
value: &str,
) -> anyhow::Result<()> {
// The JSON value is stored as a string, so we need to deserialize it.
let value: serde_json::Map<String, serde_json::Value> = 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(),
}
}