rustauth_telemetry/
lib.rs1mod auth_config;
46mod detectors;
47mod env;
48mod project_id;
49mod transport;
50pub mod types;
51mod utils;
52
53#[doc(hidden)]
54pub use auth_config::get_telemetry_auth_config;
55pub use types::{
56 CustomTrackFn, TelemetryContext, TelemetryEvent, TelemetryHttpError, TelemetryHttpTransport,
57};
58#[doc(hidden)]
59pub use types::{DetectionInfo, RuntimeInfo, TelemetryTestHooks};
60
61use std::future::Future;
62use std::pin::Pin;
63use std::sync::Arc;
64
65use rustauth_core::options::RustAuthOptions;
66use serde_json::json;
67use tokio::sync::{watch, Mutex};
68
69use crate::project_id::resolve_project_id;
70#[cfg(not(feature = "http"))]
71use crate::transport::NoopTransport;
72#[cfg(feature = "http")]
73use crate::transport::ReqwestTelemetryTransport;
74
75pub const VERSION: &str = env!("CARGO_PKG_VERSION");
77
78type TrackFn =
79 Arc<dyn Fn(TelemetryEvent) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
80
81#[derive(Clone)]
83pub struct TelemetryPublisher {
84 hard_noop: bool,
85 enabled: bool,
86 anonymous_id: Arc<Mutex<Option<String>>>,
87 base_url: Option<String>,
88 test_anonymous_id: Option<String>,
89 track: TrackFn,
90 init_done: watch::Receiver<bool>,
91}
92
93impl TelemetryPublisher {
94 pub fn noop() -> Self {
96 Self {
97 hard_noop: true,
98 enabled: false,
99 anonymous_id: Arc::new(Mutex::new(None)),
100 base_url: None,
101 test_anonymous_id: None,
102 track: Arc::new(|_| Box::pin(async move {})),
103 init_done: watch::channel(true).1,
104 }
105 }
106
107 pub async fn publish(&self, event: TelemetryEvent) {
108 if self.hard_noop || !self.enabled {
109 return;
110 }
111 if !*self.init_done.borrow() {
112 let mut init_done = self.init_done.clone();
113 let _ = init_done.changed().await;
114 }
115 let mut guard = self.anonymous_id.lock().await;
116 if guard.is_none() {
117 let id = self
118 .test_anonymous_id
119 .clone()
120 .unwrap_or_else(|| resolve_project_id(self.base_url.as_deref()));
121 *guard = Some(id);
122 }
123 let anonymous_id = guard.clone().unwrap_or_default();
124 drop(guard);
125 let TelemetryEvent {
126 event_type,
127 payload,
128 ..
129 } = event;
130 let full = TelemetryEvent {
131 event_type,
132 anonymous_id: Some(anonymous_id),
133 payload,
134 };
135 (self.track)(full).await;
136 }
137}
138
139fn resolve_transport(context: &TelemetryContext) -> Arc<dyn TelemetryHttpTransport> {
140 if let Some(client) = &context.http_transport {
141 return client.clone();
142 }
143 #[cfg(feature = "http")]
144 {
145 Arc::new(ReqwestTelemetryTransport::default())
146 }
147 #[cfg(not(feature = "http"))]
148 {
149 return Arc::new(NoopTransport);
150 }
151}
152
153async fn is_enabled(options: &RustAuthOptions, context: &TelemetryContext) -> bool {
154 let env_setting = crate::env::telemetry_env_setting();
159 if env_setting == Some(false) {
160 return false;
161 }
162 let opt_on = options.telemetry.enabled.unwrap_or(false);
163 let allow_under_test = context.skip_test_check || !crate::env::is_test();
164 (env_setting == Some(true) || opt_on) && allow_under_test
165}
166
167fn debug_enabled(options: &RustAuthOptions) -> bool {
168 options.telemetry.debug || crate::env::telemetry_debug_env()
169}
170
171fn build_track_fn(
172 context: &TelemetryContext,
173 endpoint: Option<String>,
174 debug_mode: bool,
175 transport: Arc<dyn TelemetryHttpTransport>,
176) -> TrackFn {
177 let custom = context.custom_track.clone();
178 Arc::new(move |event: TelemetryEvent| {
179 let custom = custom.clone();
180 let endpoint = endpoint.clone();
181 let transport = transport.clone();
182 Box::pin(async move {
183 if let Some(cb) = custom {
184 let _ = tokio::spawn(async move { cb(event).await }).await;
185 return;
186 }
187 let Some(url) = endpoint else {
188 return;
189 };
190 let Ok(body) = event.to_json_value() else {
191 return;
192 };
193 if debug_mode {
194 eprintln!(
195 "telemetry event {}",
196 serde_json::to_string_pretty(&body).unwrap_or_default()
197 );
198 return;
199 }
200 let _ = transport.post_json(&url, &body).await;
201 })
202 })
203}
204
205fn runtime_for(context: &TelemetryContext) -> RuntimeInfo {
206 context
207 .test_hooks
208 .as_ref()
209 .and_then(|h| h.runtime.clone())
210 .unwrap_or_else(detectors::detect_runtime)
211}
212
213fn database_for(context: &TelemetryContext) -> Option<DetectionInfo> {
214 context
215 .test_hooks
216 .as_ref()
217 .and_then(|h| h.database.clone())
218 .unwrap_or_else(detectors::detect_database)
219}
220
221fn framework_for(context: &TelemetryContext) -> Option<DetectionInfo> {
222 context
223 .test_hooks
224 .as_ref()
225 .and_then(|h| h.framework.clone())
226 .unwrap_or_else(detectors::detect_framework)
227}
228
229fn environment_for(context: &TelemetryContext) -> String {
230 context
231 .test_hooks
232 .as_ref()
233 .and_then(|h| h.environment.clone())
234 .unwrap_or_else(detectors::detect_environment)
235}
236
237fn system_info_for(context: &TelemetryContext) -> serde_json::Value {
238 context
239 .test_hooks
240 .as_ref()
241 .and_then(|h| h.system_info.clone())
242 .unwrap_or_else(detectors::detect_system_info)
243}
244
245fn package_manager_for(context: &TelemetryContext) -> Option<DetectionInfo> {
246 context
247 .test_hooks
248 .as_ref()
249 .and_then(|h| h.package_manager.clone())
250 .unwrap_or_else(detectors::detect_package_manager)
251}
252
253pub async fn create_telemetry(
255 options: &RustAuthOptions,
256 context: TelemetryContext,
257) -> TelemetryPublisher {
258 let endpoint = crate::env::telemetry_endpoint();
259 if endpoint.is_none() && context.custom_track.is_none() {
260 return TelemetryPublisher::noop();
261 }
262
263 let enabled = is_enabled(options, &context).await;
264 let transport = resolve_transport(&context);
265 let track = build_track_fn(&context, endpoint, debug_enabled(options), transport);
266
267 let test_anonymous_id = context
268 .test_hooks
269 .as_ref()
270 .and_then(|h| h.anonymous_id.clone());
271
272 let anonymous_id_cell = Arc::new(Mutex::new(None));
273 let (init_done_tx, init_done_rx) = watch::channel(!enabled);
274
275 if enabled {
276 let aid = test_anonymous_id
277 .clone()
278 .unwrap_or_else(|| resolve_project_id(options.base_url.as_deref()));
279 {
280 let mut g = anonymous_id_cell.lock().await;
281 *g = Some(aid.clone());
282 }
283
284 let payload = json!({
285 "config": get_telemetry_auth_config(options, &context),
286 "runtime": runtime_for(&context),
287 "database": database_for(&context),
288 "framework": framework_for(&context),
289 "environment": environment_for(&context),
290 "systemInfo": system_info_for(&context),
291 "packageManager": package_manager_for(&context),
292 });
293
294 let init = TelemetryEvent {
295 event_type: "init".to_owned(),
296 anonymous_id: Some(aid),
297 payload,
298 };
299 let track_init = track.clone();
300 tokio::spawn(async move {
301 track_init(init).await;
302 let _ = init_done_tx.send(true);
303 });
304 }
305
306 TelemetryPublisher {
307 hard_noop: false,
308 enabled,
309 anonymous_id: anonymous_id_cell,
310 base_url: options.base_url.clone(),
311 test_anonymous_id,
312 track,
313 init_done: init_done_rx,
314 }
315}