mod auth_config;
mod detectors;
mod env;
mod project_id;
mod transport;
pub mod types;
mod utils;
pub use auth_config::get_telemetry_auth_config;
pub use types::{
CustomTrackFn, DetectionInfo, RuntimeInfo, TelemetryContext, TelemetryEvent,
TelemetryHttpError, TelemetryHttpTransport, TelemetryTestHooks,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use openauth_core::options::OpenAuthOptions;
use serde_json::json;
use tokio::sync::Mutex;
use crate::project_id::resolve_project_id;
#[cfg(not(feature = "http"))]
use crate::transport::NoopTransport;
#[cfg(feature = "http")]
use crate::transport::ReqwestTelemetryTransport;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
type TrackFn =
Arc<dyn Fn(TelemetryEvent) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
pub struct TelemetryPublisher {
hard_noop: bool,
enabled: bool,
anonymous_id: Arc<Mutex<Option<String>>>,
base_url: Option<String>,
test_anonymous_id: Option<String>,
track: TrackFn,
}
impl TelemetryPublisher {
pub fn noop() -> Self {
Self {
hard_noop: true,
enabled: false,
anonymous_id: Arc::new(Mutex::new(None)),
base_url: None,
test_anonymous_id: None,
track: Arc::new(|_| Box::pin(async move {})),
}
}
pub async fn publish(&self, event: TelemetryEvent) {
if self.hard_noop || !self.enabled {
return;
}
let mut guard = self.anonymous_id.lock().await;
if guard.is_none() {
let id = self
.test_anonymous_id
.clone()
.unwrap_or_else(|| resolve_project_id(self.base_url.as_deref()));
*guard = Some(id);
}
let anonymous_id = guard.clone().unwrap_or_default();
drop(guard);
let TelemetryEvent {
event_type,
payload,
..
} = event;
let full = TelemetryEvent {
event_type,
anonymous_id: Some(anonymous_id),
payload,
};
(self.track)(full).await;
}
}
fn resolve_transport(context: &TelemetryContext) -> Arc<dyn TelemetryHttpTransport> {
if let Some(client) = &context.http_transport {
return client.clone();
}
#[cfg(feature = "http")]
{
Arc::new(ReqwestTelemetryTransport::default())
}
#[cfg(not(feature = "http"))]
{
return Arc::new(NoopTransport);
}
}
async fn is_enabled(options: &OpenAuthOptions, context: &TelemetryContext) -> bool {
let env_on = crate::env::telemetry_enabled_env();
let opt_on = options.telemetry.enabled.unwrap_or(false);
let allow_under_test = context.skip_test_check || !crate::env::is_test();
(env_on || opt_on) && allow_under_test
}
fn debug_enabled(options: &OpenAuthOptions) -> bool {
options.telemetry.debug || crate::env::telemetry_debug_env()
}
fn build_track_fn(
context: &TelemetryContext,
endpoint: Option<String>,
debug_mode: bool,
transport: Arc<dyn TelemetryHttpTransport>,
) -> TrackFn {
let custom = context.custom_track.clone();
Arc::new(move |event: TelemetryEvent| {
let custom = custom.clone();
let endpoint = endpoint.clone();
let transport = transport.clone();
Box::pin(async move {
if let Some(cb) = custom {
let _ = tokio::spawn(async move { cb(event).await }).await;
return;
}
let Some(url) = endpoint else {
return;
};
let Ok(body) = event.to_json_value() else {
return;
};
if debug_mode {
eprintln!(
"telemetry event {}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
return;
}
let _ = transport.post_json(&url, &body).await;
})
})
}
fn runtime_for(context: &TelemetryContext) -> RuntimeInfo {
context
.test_hooks
.as_ref()
.and_then(|h| h.runtime.clone())
.unwrap_or_else(detectors::detect_runtime)
}
fn database_for(context: &TelemetryContext) -> Option<DetectionInfo> {
context
.test_hooks
.as_ref()
.and_then(|h| h.database.clone())
.unwrap_or_else(detectors::detect_database)
}
fn framework_for(context: &TelemetryContext) -> Option<DetectionInfo> {
context
.test_hooks
.as_ref()
.and_then(|h| h.framework.clone())
.unwrap_or_else(detectors::detect_framework)
}
fn environment_for(context: &TelemetryContext) -> String {
context
.test_hooks
.as_ref()
.and_then(|h| h.environment.clone())
.unwrap_or_else(detectors::detect_environment)
}
fn system_info_for(context: &TelemetryContext) -> serde_json::Value {
context
.test_hooks
.as_ref()
.and_then(|h| h.system_info.clone())
.unwrap_or_else(detectors::detect_system_info)
}
fn package_manager_for(context: &TelemetryContext) -> Option<DetectionInfo> {
context
.test_hooks
.as_ref()
.and_then(|h| h.package_manager.clone())
.unwrap_or_else(detectors::detect_package_manager)
}
pub async fn create_telemetry(
options: &OpenAuthOptions,
context: TelemetryContext,
) -> TelemetryPublisher {
let endpoint = crate::env::telemetry_endpoint();
if endpoint.is_none() && context.custom_track.is_none() {
return TelemetryPublisher::noop();
}
let enabled = is_enabled(options, &context).await;
let transport = resolve_transport(&context);
let track = build_track_fn(&context, endpoint, debug_enabled(options), transport);
let test_anonymous_id = context
.test_hooks
.as_ref()
.and_then(|h| h.anonymous_id.clone());
let anonymous_id_cell = Arc::new(Mutex::new(None));
if enabled {
let aid = test_anonymous_id
.clone()
.unwrap_or_else(|| resolve_project_id(options.base_url.as_deref()));
{
let mut g = anonymous_id_cell.lock().await;
*g = Some(aid.clone());
}
let payload = json!({
"config": get_telemetry_auth_config(options, &context),
"runtime": runtime_for(&context),
"database": database_for(&context),
"framework": framework_for(&context),
"environment": environment_for(&context),
"systemInfo": system_info_for(&context),
"packageManager": package_manager_for(&context),
});
let init = TelemetryEvent {
event_type: "init".to_owned(),
anonymous_id: Some(aid),
payload,
};
let track_init = track.clone();
tokio::spawn(async move {
track_init(init).await;
});
}
TelemetryPublisher {
hard_noop: false,
enabled,
anonymous_id: anonymous_id_cell,
base_url: options.base_url.clone(),
test_anonymous_id,
track,
}
}