1use std::any::TypeId;
2use std::borrow::Cow;
3#[cfg(any(feature = "logs", feature = "metrics"))]
4use std::collections::BTreeMap;
5use std::fmt;
6use std::panic::RefUnwindSafe;
7use std::sync::Arc;
8#[cfg(any(feature = "logs", feature = "metrics", feature = "release-health"))]
9use std::sync::RwLock;
10use std::time::Duration;
11
12#[cfg(feature = "metrics")]
13use crate::metrics::IntoProtocolMetric;
14#[cfg(feature = "release-health")]
15use crate::protocol::SessionUpdate;
16use crate::transport::TransportOptions;
17use rand::random;
18use sentry_types::protocol::v7::client_report::{
19 Category as ClientReportCategory, LossSource, Reason as ClientReportReason,
20};
21use sentry_types::random_uuid;
22
23#[cfg(any(feature = "logs", feature = "metrics"))]
24use self::batcher::Batcher;
25use crate::constants::SDK_INFO;
26use crate::protocol::{ClientSdkInfo, Event};
27#[cfg(feature = "release-health")]
28use crate::session::SessionFlusher;
29use crate::types::{Dsn, Uuid};
30#[cfg(feature = "release-health")]
31use crate::SessionMode;
32use crate::{ClientOptions, Envelope, Hub, Integration, Scope};
33
34#[cfg(feature = "logs")]
35use sentry_types::protocol::v7::Context;
36#[cfg(feature = "logs")]
37use sentry_types::protocol::v7::Log;
38#[cfg(any(feature = "logs", feature = "metrics"))]
39use sentry_types::protocol::v7::LogAttribute;
40#[cfg(feature = "metrics")]
41use sentry_types::protocol::v7::Metric;
42
43mod batcher;
44mod envelope_sender;
45
46pub(crate) mod client_reports;
47
48pub(crate) use self::envelope_sender::EnvelopeSender;
49
50impl<T: Into<ClientOptions>> From<T> for Client {
51 fn from(o: T) -> Client {
52 Client::with_options(o.into())
53 }
54}
55
56pub struct Client {
74 options: ClientOptions,
75 envelope_sender: EnvelopeSender,
76 #[cfg(feature = "release-health")]
77 session_flusher: RwLock<Option<SessionFlusher>>,
78 #[cfg(feature = "logs")]
79 logs_batcher: RwLock<Option<Batcher<Log>>>,
80 #[cfg(feature = "metrics")]
81 metrics_batcher: RwLock<Option<Batcher<Metric>>>,
82 #[cfg(feature = "logs")]
83 default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
84 #[cfg(feature = "metrics")]
85 default_metric_attributes: BTreeMap<Cow<'static, str>, LogAttribute>,
86 integrations: Vec<(TypeId, Arc<dyn Integration>)>,
87 pub(crate) sdk_info: ClientSdkInfo,
88}
89
90impl fmt::Debug for Client {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.debug_struct("Client")
93 .field("dsn", &self.dsn())
94 .field("options", &self.options)
95 .finish()
96 }
97}
98
99impl Clone for Client {
100 fn clone(&self) -> Client {
101 let envelope_sender = self.envelope_sender.clone_with_new_transport_slot();
102
103 #[cfg(feature = "release-health")]
104 let session_flusher = RwLock::new(Some(SessionFlusher::new(
105 envelope_sender.clone(),
106 self.options.session_mode,
107 )));
108
109 #[cfg(feature = "logs")]
110 let logs_batcher = RwLock::new(if self.options.enable_logs {
111 Some(Batcher::new(envelope_sender.clone()))
112 } else {
113 None
114 });
115
116 #[cfg(feature = "metrics")]
117 let metrics_batcher = RwLock::new(
118 self.options
119 .enable_metrics
120 .then(|| Batcher::new(envelope_sender.clone())),
121 );
122
123 Client {
124 options: self.options.clone(),
125 envelope_sender,
126 #[cfg(feature = "release-health")]
127 session_flusher,
128 #[cfg(feature = "logs")]
129 logs_batcher,
130 #[cfg(feature = "metrics")]
131 metrics_batcher,
132 #[cfg(feature = "logs")]
133 default_log_attributes: self.default_log_attributes.clone(),
134 #[cfg(feature = "metrics")]
135 default_metric_attributes: self.default_metric_attributes.clone(),
136 integrations: self.integrations.clone(),
137 sdk_info: self.sdk_info.clone(),
138 }
139 }
140}
141
142impl Client {
143 pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
164 Client::with_options(opts.into())
165 }
166
167 pub fn with_options(mut options: ClientOptions) -> Client {
172 Hub::with_current(|_| {});
175
176 let envelope_sender = build_envelope_sender(&options);
177 let mut sdk_info = SDK_INFO.clone();
178
179 let integrations: Vec<_> = options
182 .integrations
183 .iter()
184 .map(|integration| (integration.as_ref().type_id(), integration.clone()))
185 .collect();
186
187 for (_, integration) in integrations.iter() {
188 integration.setup(&mut options);
189 sdk_info.integrations.push(integration.name().to_string());
190 }
191
192 #[cfg(feature = "release-health")]
193 let session_flusher = RwLock::new(Some(SessionFlusher::new(
194 envelope_sender.clone(),
195 options.session_mode,
196 )));
197
198 #[cfg(feature = "logs")]
199 let logs_batcher = RwLock::new(if options.enable_logs {
200 Some(Batcher::new(envelope_sender.clone()))
201 } else {
202 None
203 });
204
205 #[cfg(feature = "metrics")]
206 let metrics_batcher = RwLock::new(
207 options
208 .enable_metrics
209 .then(|| Batcher::new(envelope_sender.clone())),
210 );
211
212 #[allow(unused_mut)]
213 let mut client = Client {
214 options,
215 envelope_sender,
216 #[cfg(feature = "release-health")]
217 session_flusher,
218 #[cfg(feature = "logs")]
219 logs_batcher,
220 #[cfg(feature = "metrics")]
221 metrics_batcher,
222 #[cfg(feature = "logs")]
223 default_log_attributes: None,
224 #[cfg(feature = "metrics")]
225 default_metric_attributes: Default::default(),
226 integrations,
227 sdk_info,
228 };
229
230 #[cfg(feature = "logs")]
231 client.cache_default_log_attributes();
232
233 #[cfg(feature = "metrics")]
234 client.cache_default_metric_attributes();
235
236 client
237 }
238
239 #[cfg(feature = "logs")]
240 fn cache_default_log_attributes(&mut self) {
241 let mut attributes = BTreeMap::new();
242
243 if let Some(environment) = self.options.environment.as_ref() {
244 attributes.insert("sentry.environment".to_owned(), environment.clone().into());
245 }
246
247 if let Some(release) = self.options.release.as_ref() {
248 attributes.insert("sentry.release".to_owned(), release.clone().into());
249 }
250
251 attributes.insert(
252 "sentry.sdk.name".to_owned(),
253 self.sdk_info.name.to_owned().into(),
254 );
255
256 attributes.insert(
257 "sentry.sdk.version".to_owned(),
258 self.sdk_info.version.to_owned().into(),
259 );
260
261 let mut fake_event = Event::default();
268 for (_, integration) in self.integrations.iter() {
269 if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
270 fake_event = res;
271 }
272 }
273
274 if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
275 if let Some(name) = os.name.as_ref() {
276 attributes.insert("os.name".to_owned(), name.to_owned().into());
277 }
278 if let Some(version) = os.version.as_ref() {
279 attributes.insert("os.version".to_owned(), version.to_owned().into());
280 }
281 }
282
283 if let Some(server) = &self.options.server_name {
284 attributes.insert("server.address".to_owned(), server.clone().into());
285 }
286
287 self.default_log_attributes = Some(attributes);
288 }
289
290 #[cfg(feature = "metrics")]
291 fn cache_default_metric_attributes(&mut self) {
292 let always_present_attributes = [
293 ("sentry.sdk.name", &self.sdk_info.name),
294 ("sentry.sdk.version", &self.sdk_info.version),
295 ]
296 .into_iter()
297 .map(|(name, value)| (name.into(), value.as_str().into()));
298
299 let maybe_present_attributes = [
300 ("sentry.environment", &self.options.environment),
301 ("sentry.release", &self.options.release),
302 ("server.address", &self.options.server_name),
303 ]
304 .into_iter()
305 .filter_map(|(name, value)| value.clone().map(|value| (name.into(), value.into())));
306
307 self.default_metric_attributes = maybe_present_attributes
308 .chain(always_present_attributes)
309 .collect();
310 }
311
312 pub(crate) fn get_integration<I>(&self) -> Option<&I>
313 where
314 I: Integration,
315 {
316 let id = TypeId::of::<I>();
317 let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
318 integration.as_ref().as_any().downcast_ref()
319 }
320
321 pub fn prepare_event(
323 &self,
324 mut event: Event<'static>,
325 scope: Option<&Scope>,
326 ) -> Option<Event<'static>> {
327 if event.event_id.is_nil() {
330 event.event_id = random_uuid();
331 }
332
333 if event.sdk.is_none() {
334 event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
336 }
337
338 if let Some(scope) = scope {
339 event = match scope.apply_to_event(event) {
340 Some(event) => event,
341 None => {
342 self.record_lost_event(ClientReportReason::EventProcessor);
343 return None;
344 }
345 };
346 }
347
348 for (_, integration) in self.integrations.iter() {
349 let id = event.event_id;
350 event = match integration.process_event(event, &self.options) {
351 Some(event) => event,
352 None => {
353 sentry_debug!("integration dropped event {:?}", id);
354 self.record_lost_event(ClientReportReason::EventProcessor);
355 return None;
356 }
357 }
358 }
359
360 if event.release.is_none() {
361 event.release.clone_from(&self.options.release);
362 }
363 if event.environment.is_none() {
364 event.environment.clone_from(&self.options.environment);
365 }
366 if event.server_name.is_none() {
367 event.server_name.clone_from(&self.options.server_name);
368 }
369 if &event.platform == "other" {
370 event.platform = "native".into();
371 }
372
373 if let Some(ref func) = self.options.before_send {
374 sentry_debug!("invoking before_send callback");
375 let id = event.event_id;
376 if let Some(processed_event) = func(event) {
377 event = processed_event;
378 } else {
379 sentry_debug!("before_send dropped event {:?}", id);
380 self.record_lost_event(ClientReportReason::BeforeSend);
381 return None;
382 }
383 }
384
385 if let Some(scope) = scope {
386 scope.update_session_from_event(&event);
387 }
388
389 if !self.sample_should_send(self.options.sample_rate) {
390 self.record_lost_event(ClientReportReason::SampleRate);
391 None
392 } else {
393 Some(event)
394 }
395 }
396
397 pub fn options(&self) -> &ClientOptions {
399 &self.options
400 }
401
402 pub fn dsn(&self) -> Option<&Dsn> {
404 self.options.dsn.as_ref()
405 }
406
407 pub fn is_enabled(&self) -> bool {
431 self.options.dsn.is_some() && self.envelope_sender.is_enabled()
432 }
433
434 pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
436 let mut event_id = Default::default();
437 self.envelope_sender.send_envelope_with(|| {
438 self.prepare_event(event, scope).map(|event| {
439 event_id = event.event_id;
440 let mut envelope: Envelope = event.into();
441 #[cfg(feature = "release-health")]
444 if self.options.session_mode == SessionMode::Application {
445 let session_item = scope.and_then(|scope| {
446 scope
447 .session
448 .lock()
449 .unwrap()
450 .as_mut()
451 .and_then(|session| session.create_envelope_item())
452 });
453 if let Some(session_item) = session_item {
454 envelope.add_item(session_item);
455 }
456 }
457
458 if let Some(scope) = scope {
459 for attachment in scope.attachments.iter().cloned() {
460 envelope.add_item(attachment);
461 }
462 }
463
464 envelope
465 })
466 });
467 event_id
468 }
469
470 pub(crate) fn record_lost_data<L>(&self, data: &L, reason: ClientReportReason)
471 where
472 L: LossSource + ?Sized,
473 {
474 self.envelope_sender.record_lost_data(data, reason);
475 }
476
477 fn record_loss(
479 &self,
480 category: ClientReportCategory,
481 reason: ClientReportReason,
482 quantity: u64,
483 ) {
484 self.envelope_sender.record_loss(category, reason, quantity);
485 }
486
487 fn record_lost_event(&self, reason: ClientReportReason) {
489 self.record_loss(ClientReportCategory::Error, reason, 1);
490 }
491
492 pub fn send_envelope(&self, envelope: Envelope) {
494 self.envelope_sender.send_envelope(envelope);
495 }
496
497 #[cfg(feature = "release-health")]
498 pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
499 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
500 flusher.enqueue(session_update);
501 }
502 }
503
504 pub fn flush(&self, timeout: Option<Duration>) -> bool {
506 #[cfg(feature = "release-health")]
507 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
508 flusher.flush();
509 }
510 #[cfg(feature = "logs")]
511 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
512 batcher.flush();
513 }
514 #[cfg(feature = "metrics")]
515 if let Some(ref batcher) = *self.metrics_batcher.read().unwrap() {
516 batcher.flush();
517 }
518 self.envelope_sender
519 .flush(timeout.unwrap_or(self.options.shutdown_timeout))
520 }
521
522 pub fn close(&self, timeout: Option<Duration>) -> bool {
530 #[cfg(feature = "release-health")]
531 drop(self.session_flusher.write().unwrap().take());
532 #[cfg(feature = "logs")]
533 drop(self.logs_batcher.write().unwrap().take());
534 #[cfg(feature = "metrics")]
535 drop(self.metrics_batcher.write().unwrap().take());
536 self.envelope_sender
537 .shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
538 }
539
540 pub fn sample_should_send(&self, rate: f32) -> bool {
543 if rate >= 1.0 {
544 true
545 } else if rate <= 0.0 {
546 false
547 } else {
548 random::<f32>() < rate
549 }
550 }
551
552 #[cfg(feature = "logs")]
554 pub fn capture_log(&self, log: Log, scope: &Scope) {
555 if !self.options.enable_logs {
556 sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
557 return;
558 }
559 if let Some(log) = self.prepare_log(log, scope) {
560 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
561 batcher.enqueue(log);
562 }
563 }
564 }
565
566 #[cfg(feature = "logs")]
569 fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
570 scope.apply_to_log(&mut log);
571
572 if let Some(default_attributes) = self.default_log_attributes.as_ref() {
573 for (key, val) in default_attributes.iter() {
574 log.attributes.entry(key.to_owned()).or_insert(val.clone());
575 }
576 }
577
578 if let Some(ref func) = self.options.before_send_log {
579 let losses: Vec<_> = log.losses().collect();
580 log = match func(log) {
581 Some(log) => log,
582 None => {
583 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
584 return None;
585 }
586 };
587 }
588
589 Some(log)
590 }
591
592 #[cfg(feature = "metrics")]
594 pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
595 if !self.options.enable_metrics {
596 return;
598 }
599
600 if let Some(metric) = self.prepare_metric(metric, scope) {
601 if let Some(batcher) = self
602 .metrics_batcher
603 .read()
604 .expect("metrics batcher lock could not be acquired")
605 .as_ref()
606 {
607 batcher.enqueue(metric);
608 }
609 }
610 }
611
612 #[cfg(feature = "metrics")]
615 fn prepare_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) -> Option<Metric> {
616 let mut metric = scope.apply_to_metric(metric, self.options().send_default_pii);
617
618 for (key, val) in &self.default_metric_attributes {
619 metric.attributes.entry(key.clone()).or_insert(val.clone());
620 }
621
622 if let Some(ref func) = self.options.before_send_metric {
623 let losses: Vec<_> = metric.losses().collect();
624 metric = match func(metric) {
625 Some(metric) => metric,
626 None => {
627 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
628 return None;
629 }
630 };
631 }
632
633 Some(metric)
634 }
635}
636
637impl RefUnwindSafe for Client {}
640
641fn build_envelope_sender(client_options: &ClientOptions) -> EnvelopeSender {
645 let ClientOptions {
646 dsn,
647 transport: transport_factory,
648 user_agent,
649 http_proxy,
650 https_proxy,
651 accept_invalid_certs,
652 ..
653 } = client_options;
654
655 match (dsn.as_ref(), transport_factory.as_ref()) {
656 (Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|client_report_recorder| {
657 let options = TransportOptions {
658 dsn: dsn.clone(),
659 user_agent: user_agent.clone(),
660 http_proxy: http_proxy.clone(),
661 https_proxy: https_proxy.clone(),
662 accept_invalid_certs: *accept_invalid_certs,
663 client_report_recorder,
664 };
665
666 transport_factory.create_transport_with_options(options)
667 }),
668 _ => Default::default(),
669 }
670}