1use std::any::TypeId;
2use std::borrow::Cow;
3#[cfg(feature = "logs")]
4use std::collections::BTreeMap;
5use std::fmt;
6use std::panic::RefUnwindSafe;
7use std::sync::{Arc, RwLock};
8use std::time::Duration;
9
10#[cfg(feature = "release-health")]
11use crate::protocol::SessionUpdate;
12use rand::random;
13use sentry_types::random_uuid;
14
15use crate::constants::SDK_INFO;
16#[cfg(feature = "logs")]
17use crate::logs::LogsBatcher;
18use crate::protocol::{ClientSdkInfo, Event};
19#[cfg(feature = "release-health")]
20use crate::session::SessionFlusher;
21use crate::types::{Dsn, Uuid};
22#[cfg(feature = "release-health")]
23use crate::SessionMode;
24use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport};
25#[cfg(feature = "logs")]
26use sentry_types::protocol::v7::Context;
27#[cfg(feature = "logs")]
28use sentry_types::protocol::v7::{Log, LogAttribute};
29
30impl<T: Into<ClientOptions>> From<T> for Client {
31 fn from(o: T) -> Client {
32 Client::with_options(o.into())
33 }
34}
35
36pub(crate) type TransportArc = Arc<RwLock<Option<Arc<dyn Transport>>>>;
37
38pub struct Client {
56 options: ClientOptions,
57 transport: TransportArc,
58 #[cfg(feature = "release-health")]
59 session_flusher: RwLock<Option<SessionFlusher>>,
60 #[cfg(feature = "logs")]
61 logs_batcher: RwLock<Option<LogsBatcher>>,
62 #[cfg(feature = "logs")]
63 default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
64 integrations: Vec<(TypeId, Arc<dyn Integration>)>,
65 pub(crate) sdk_info: ClientSdkInfo,
66}
67
68impl fmt::Debug for Client {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("Client")
71 .field("dsn", &self.dsn())
72 .field("options", &self.options)
73 .finish()
74 }
75}
76
77impl Clone for Client {
78 fn clone(&self) -> Client {
79 let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone()));
80
81 #[cfg(feature = "release-health")]
82 let session_flusher = RwLock::new(Some(SessionFlusher::new(
83 transport.clone(),
84 self.options.session_mode,
85 )));
86
87 #[cfg(feature = "logs")]
88 let logs_batcher = RwLock::new(if self.options.enable_logs {
89 Some(LogsBatcher::new(transport.clone()))
90 } else {
91 None
92 });
93
94 Client {
95 options: self.options.clone(),
96 transport,
97 #[cfg(feature = "release-health")]
98 session_flusher,
99 #[cfg(feature = "logs")]
100 logs_batcher,
101 #[cfg(feature = "logs")]
102 default_log_attributes: self.default_log_attributes.clone(),
103 integrations: self.integrations.clone(),
104 sdk_info: self.sdk_info.clone(),
105 }
106 }
107}
108
109impl Client {
110 pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
131 Client::with_options(opts.into())
132 }
133
134 pub fn with_options(mut options: ClientOptions) -> Client {
139 Hub::with(|_| {});
142
143 let create_transport = || {
144 options.dsn.as_ref()?;
145 let factory = options.transport.as_ref()?;
146 Some(factory.create_transport(&options))
147 };
148
149 let transport = Arc::new(RwLock::new(create_transport()));
150
151 let mut sdk_info = SDK_INFO.clone();
152
153 let integrations: Vec<_> = options
156 .integrations
157 .iter()
158 .map(|integration| (integration.as_ref().type_id(), integration.clone()))
159 .collect();
160
161 for (_, integration) in integrations.iter() {
162 integration.setup(&mut options);
163 sdk_info.integrations.push(integration.name().to_string());
164 }
165
166 #[cfg(feature = "release-health")]
167 let session_flusher = RwLock::new(Some(SessionFlusher::new(
168 transport.clone(),
169 options.session_mode,
170 )));
171
172 #[cfg(feature = "logs")]
173 let logs_batcher = RwLock::new(if options.enable_logs {
174 Some(LogsBatcher::new(transport.clone()))
175 } else {
176 None
177 });
178
179 #[allow(unused_mut)]
180 let mut client = Client {
181 options,
182 transport,
183 #[cfg(feature = "release-health")]
184 session_flusher,
185 #[cfg(feature = "logs")]
186 logs_batcher,
187 #[cfg(feature = "logs")]
188 default_log_attributes: None,
189 integrations,
190 sdk_info,
191 };
192
193 #[cfg(feature = "logs")]
194 client.cache_default_log_attributes();
195
196 client
197 }
198
199 #[cfg(feature = "logs")]
200 fn cache_default_log_attributes(&mut self) {
201 let mut attributes = BTreeMap::new();
202
203 if let Some(environment) = self.options.environment.as_ref() {
204 attributes.insert("sentry.environment".to_owned(), environment.clone().into());
205 }
206
207 if let Some(release) = self.options.release.as_ref() {
208 attributes.insert("sentry.release".to_owned(), release.clone().into());
209 }
210
211 attributes.insert(
212 "sentry.sdk.name".to_owned(),
213 self.sdk_info.name.to_owned().into(),
214 );
215
216 attributes.insert(
217 "sentry.sdk.version".to_owned(),
218 self.sdk_info.version.to_owned().into(),
219 );
220
221 let mut fake_event = Event::default();
228 for (_, integration) in self.integrations.iter() {
229 if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
230 fake_event = res;
231 }
232 }
233
234 if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
235 if let Some(name) = os.name.as_ref() {
236 attributes.insert("os.name".to_owned(), name.to_owned().into());
237 }
238 if let Some(version) = os.version.as_ref() {
239 attributes.insert("os.version".to_owned(), version.to_owned().into());
240 }
241 }
242
243 if let Some(server) = &self.options.server_name {
244 attributes.insert("server.address".to_owned(), server.clone().into());
245 }
246
247 self.default_log_attributes = Some(attributes);
248 }
249
250 pub(crate) fn get_integration<I>(&self) -> Option<&I>
251 where
252 I: Integration,
253 {
254 let id = TypeId::of::<I>();
255 let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
256 integration.as_ref().as_any().downcast_ref()
257 }
258
259 pub fn prepare_event(
261 &self,
262 mut event: Event<'static>,
263 scope: Option<&Scope>,
264 ) -> Option<Event<'static>> {
265 if event.event_id.is_nil() {
268 event.event_id = random_uuid();
269 }
270
271 if event.sdk.is_none() {
272 event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
274 }
275
276 if let Some(scope) = scope {
277 event = scope.apply_to_event(event)?;
278 }
279
280 for (_, integration) in self.integrations.iter() {
281 let id = event.event_id;
282 event = match integration.process_event(event, &self.options) {
283 Some(event) => event,
284 None => {
285 sentry_debug!("integration dropped event {:?}", id);
286 return None;
287 }
288 }
289 }
290
291 if event.release.is_none() {
292 event.release.clone_from(&self.options.release);
293 }
294 if event.environment.is_none() {
295 event.environment.clone_from(&self.options.environment);
296 }
297 if event.server_name.is_none() {
298 event.server_name.clone_from(&self.options.server_name);
299 }
300 if &event.platform == "other" {
301 event.platform = "native".into();
302 }
303
304 if let Some(ref func) = self.options.before_send {
305 sentry_debug!("invoking before_send callback");
306 let id = event.event_id;
307 if let Some(processed_event) = func(event) {
308 event = processed_event;
309 } else {
310 sentry_debug!("before_send dropped event {:?}", id);
311 return None;
312 }
313 }
314
315 if let Some(scope) = scope {
316 scope.update_session_from_event(&event);
317 }
318
319 if !self.sample_should_send(self.options.sample_rate) {
320 None
321 } else {
322 Some(event)
323 }
324 }
325
326 pub fn options(&self) -> &ClientOptions {
328 &self.options
329 }
330
331 pub fn dsn(&self) -> Option<&Dsn> {
333 self.options.dsn.as_ref()
334 }
335
336 pub fn is_enabled(&self) -> bool {
360 self.options.dsn.is_some() && self.transport.read().unwrap().is_some()
361 }
362
363 pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
365 if let Some(ref transport) = *self.transport.read().unwrap() {
366 if let Some(event) = self.prepare_event(event, scope) {
367 let event_id = event.event_id;
368 let mut envelope: Envelope = event.into();
369 #[cfg(feature = "release-health")]
372 if self.options.session_mode == SessionMode::Application {
373 let session_item = scope.and_then(|scope| {
374 scope
375 .session
376 .lock()
377 .unwrap()
378 .as_mut()
379 .and_then(|session| session.create_envelope_item())
380 });
381 if let Some(session_item) = session_item {
382 envelope.add_item(session_item);
383 }
384 }
385
386 if let Some(scope) = scope {
387 for attachment in scope.attachments.iter().cloned() {
388 envelope.add_item(attachment);
389 }
390 }
391
392 transport.send_envelope(envelope);
393 return event_id;
394 }
395 }
396 Default::default()
397 }
398
399 pub fn send_envelope(&self, envelope: Envelope) {
401 if let Some(ref transport) = *self.transport.read().unwrap() {
402 transport.send_envelope(envelope);
403 }
404 }
405
406 #[cfg(feature = "release-health")]
407 pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
408 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
409 flusher.enqueue(session_update);
410 }
411 }
412
413 pub fn flush(&self, timeout: Option<Duration>) -> bool {
415 #[cfg(feature = "release-health")]
416 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
417 flusher.flush();
418 }
419 #[cfg(feature = "logs")]
420 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
421 batcher.flush();
422 }
423 if let Some(ref transport) = *self.transport.read().unwrap() {
424 transport.flush(timeout.unwrap_or(self.options.shutdown_timeout))
425 } else {
426 true
427 }
428 }
429
430 pub fn close(&self, timeout: Option<Duration>) -> bool {
438 #[cfg(feature = "release-health")]
439 drop(self.session_flusher.write().unwrap().take());
440 #[cfg(feature = "logs")]
441 drop(self.logs_batcher.write().unwrap().take());
442 let transport_opt = self.transport.write().unwrap().take();
443 if let Some(transport) = transport_opt {
444 sentry_debug!("client close; request transport to shut down");
445 transport.shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
446 } else {
447 sentry_debug!("client close; no transport to shut down");
448 true
449 }
450 }
451
452 pub fn sample_should_send(&self, rate: f32) -> bool {
455 if rate >= 1.0 {
456 true
457 } else if rate <= 0.0 {
458 false
459 } else {
460 random::<f32>() < rate
461 }
462 }
463
464 #[cfg(feature = "logs")]
466 pub fn capture_log(&self, log: Log, scope: &Scope) {
467 if !self.options.enable_logs {
468 sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
469 return;
470 }
471 if let Some(log) = self.prepare_log(log, scope) {
472 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
473 batcher.enqueue(log);
474 }
475 }
476 }
477
478 #[cfg(feature = "logs")]
481 fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
482 scope.apply_to_log(&mut log);
483
484 if let Some(default_attributes) = self.default_log_attributes.as_ref() {
485 for (key, val) in default_attributes.iter() {
486 log.attributes.entry(key.to_owned()).or_insert(val.clone());
487 }
488 }
489
490 if let Some(ref func) = self.options.before_send_log {
491 log = func(log)?;
492 }
493
494 Some(log)
495 }
496}
497
498impl RefUnwindSafe for Client {}