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