use std::time::Duration;
use log::debug;
use reqwest::Url;
use crate::InitOptions;
#[derive(Debug, Clone)]
pub struct Config {
pub app_key: String,
pub ingest_api_url: Url,
pub flush_interval: Duration,
pub app_key_header: String,
pub sdk_name: String,
}
pub(crate) const DEFAULT_API_PATH: &str = "/v1/events";
pub(crate) const DEFAULT_APP_KEY_HEADER: &str = "App-Key";
static DISABLED_URL: &str = "http://localhost";
#[cfg(not(debug_assertions))]
static DEFAULT_FLUSH_INTERVAL: Duration = Duration::from_secs(60);
#[cfg(debug_assertions)]
static DEFAULT_FLUSH_INTERVAL: Duration = Duration::from_secs(2);
impl Config {
pub fn new(app_key: String, opts: InitOptions) -> Self {
let Some(base_url) = opts.host.clone() else {
debug!(
"No backend host configured. Set `InitOptions.host` to point \
the plugin at your analytics backend. Tracking will be \
disabled. (App Key was '{}')",
app_key
);
return Config::default();
};
let api_path = opts
.api_path
.unwrap_or_else(|| DEFAULT_API_PATH.to_string());
let ingest_api_url = match join_url(&base_url, &api_path) {
Some(url) => url,
None => {
debug!(
"Failed to build ingest URL from host '{}' and path '{}'. \
Tracking will be disabled.",
base_url, api_path
);
return Config::default();
}
};
let app_key_header = opts
.app_key_header
.unwrap_or_else(|| DEFAULT_APP_KEY_HEADER.to_string());
let sdk_name = opts.sdk_name.unwrap_or_else(|| {
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")).to_string()
});
Self {
app_key,
ingest_api_url,
flush_interval: opts.flush_interval.unwrap_or(DEFAULT_FLUSH_INTERVAL),
app_key_header,
sdk_name,
}
}
}
fn join_url(base: &str, path: &str) -> Option<Url> {
let trimmed_base = base.trim_end_matches('/');
let normalized_path = if path.starts_with('/') {
path.to_string()
} else {
format!("/{}", path)
};
Url::parse(&format!("{}{}", trimmed_base, normalized_path)).ok()
}
impl Default for Config {
fn default() -> Self {
Self {
app_key: String::new(),
ingest_api_url: Url::parse(DISABLED_URL).unwrap(),
flush_interval: DEFAULT_FLUSH_INTERVAL,
app_key_header: DEFAULT_APP_KEY_HEADER.to_string(),
sdk_name: concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")).to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn opts(host: Option<&str>) -> InitOptions {
InitOptions {
host: host.map(String::from),
..Default::default()
}
}
#[test]
fn no_host_disables_regardless_of_key() {
let cfg = Config::new("A-US-abc".into(), opts(None));
assert!(cfg.app_key.is_empty());
let cfg = Config::new("A-EU-abc".into(), opts(None));
assert!(cfg.app_key.is_empty());
let cfg = Config::new("A-SH-abc".into(), opts(None));
assert!(cfg.app_key.is_empty());
let cfg = Config::new("just-a-key".into(), opts(None));
assert!(cfg.app_key.is_empty());
}
#[test]
fn host_with_arbitrary_key_works() {
let cfg = Config::new(
"my-app-key-123".into(),
opts(Some("https://analytics.myapp.com")),
);
assert_eq!(cfg.app_key, "my-app-key-123");
assert_eq!(
cfg.ingest_api_url.as_str(),
"https://analytics.myapp.com/v1/events"
);
}
#[test]
fn host_with_region_style_key_works() {
let cfg = Config::new("A-EU-abc".into(), opts(Some("https://analytics.myapp.com")));
assert_eq!(
cfg.ingest_api_url.as_str(),
"https://analytics.myapp.com/v1/events"
);
}
#[test]
fn default_api_path_is_v1_events() {
let cfg = Config::new("k".into(), opts(Some("https://analytics.myapp.com")));
assert!(cfg.ingest_api_url.as_str().ends_with("/v1/events"));
}
#[test]
fn custom_api_path_is_used() {
let mut o = opts(Some("https://analytics.myapp.com"));
o.api_path = Some("/ingest".into());
let cfg = Config::new("k".into(), o);
assert_eq!(
cfg.ingest_api_url.as_str(),
"https://analytics.myapp.com/ingest"
);
}
#[test]
fn custom_host_with_trailing_slash() {
let cfg = Config::new("k".into(), opts(Some("https://analytics.myapp.com/")));
assert_eq!(
cfg.ingest_api_url.as_str(),
"https://analytics.myapp.com/v1/events"
);
}
#[test]
fn invalid_host_disables() {
let cfg = Config::new("k".into(), opts(Some("not a url")));
assert!(cfg.app_key.is_empty());
}
#[test]
fn custom_app_key_header() {
let mut o = opts(Some("https://x.test"));
o.app_key_header = Some("X-App-Key".into());
let cfg = Config::new("k".into(), o);
assert_eq!(cfg.app_key_header, "X-App-Key");
}
#[test]
fn default_app_key_header_is_app_key() {
let cfg = Config::new("k".into(), opts(Some("https://x.test")));
assert_eq!(cfg.app_key_header, "App-Key");
}
#[test]
fn custom_sdk_name() {
let mut o = opts(Some("https://x.test"));
o.sdk_name = Some("myapp@1.0.0".into());
let cfg = Config::new("k".into(), o);
assert_eq!(cfg.sdk_name, "myapp@1.0.0");
}
}