use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap},
time::SystemTime,
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TelemetryEventData {
pub command: String,
pub action: String,
pub message: String,
pub error_type: Option<String>,
pub error_handled: bool,
pub values: HashMap<String, serde_json::Value>,
pub time: DateTime<Utc>,
pub module: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stack_frames: Vec<StackFrame>,
}
impl TelemetryEventData {
pub fn new(name: impl ToString, message: impl ToString) -> Self {
Self {
command: std::env::args()
.nth(1)
.unwrap_or_else(|| "unknown".to_string()),
action: strip_paths(&name.to_string()),
message: strip_paths(&message.to_string()),
file: None,
module: None,
time: DateTime::<Utc>::from(SystemTime::now()),
values: HashMap::new(),
error_type: None,
column: None,
line: None,
stack_frames: vec![],
error_handled: false,
}
}
pub fn with_value<K: ToString, V: serde::Serialize>(mut self, key: K, value: V) -> Self {
let mut value = serde_json::to_value(value).unwrap();
strip_paths_value(&mut value);
self.values.insert(key.to_string(), value);
self
}
pub fn with_module(mut self, module: impl ToString) -> Self {
self.module = Some(strip_paths(&module.to_string()));
self
}
pub fn with_file(mut self, file: impl ToString) -> Self {
self.file = Some(strip_paths(&file.to_string()));
self
}
pub fn with_line_column(mut self, line: u32, column: u32) -> Self {
self.line = Some(line);
self.column = Some(column);
self
}
pub fn with_error_handled(mut self, error_handled: bool) -> Self {
self.error_handled = error_handled;
self
}
pub fn with_error_type(mut self, error_type: String) -> Self {
self.error_type = Some(error_type);
self
}
pub fn with_stack_frames(mut self, stack_frames: Vec<StackFrame>) -> Self {
self.stack_frames = stack_frames;
self
}
pub fn with_values(mut self, fields: serde_json::Map<String, serde_json::Value>) -> Self {
for (key, value) in fields {
self = self.with_value(key, value);
}
self
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap()
}
}
impl std::fmt::Display for TelemetryEventData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct StackFrame {
pub raw_id: String,
pub mangled_name: String,
pub resolved_name: String,
pub lang: String,
pub resolved: bool,
pub platform: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub function: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub module: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lineno: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub colno: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub abs_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_line: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pre_context: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub post_context: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub in_app: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instruction_addr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub addr_mode: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub vars: BTreeMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chunk_id: Option<String>,
}
pub fn strip_paths(string: &str) -> String {
let home_dir = dirs::home_dir().unwrap_or_default();
let mut cwd = std::env::current_dir().unwrap_or_default();
let mut string = string.to_string();
loop {
string = string.replace(&*cwd.to_string_lossy(), "<stripped>");
let Some(parent) = cwd.parent() else {
break;
};
cwd = parent.to_path_buf();
if cwd == home_dir {
break;
}
}
string.replace(&*home_dir.to_string_lossy(), "~")
}
fn strip_paths_value(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(s) => *s = strip_paths(s),
serde_json::Value::Object(map) => map.values_mut().for_each(strip_paths_value),
serde_json::Value::Array(arr) => arr.iter_mut().for_each(strip_paths_value),
_ => {}
}
}