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, EventSamplingStrategy, 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
50fn event_sample_rate(event_sampling_strategy: &EventSamplingStrategy) -> f32 {
52 match event_sampling_strategy {
53 &EventSamplingStrategy::FixedRate(rate) => rate,
54 }
55}
56
57impl<T: Into<ClientOptions>> From<T> for Client {
58 fn from(o: T) -> Client {
59 Client::with_options(o.into())
60 }
61}
62
63pub struct Client {
81 options: ClientOptions,
82 envelope_sender: EnvelopeSender,
83 #[cfg(feature = "release-health")]
84 session_flusher: RwLock<Option<SessionFlusher>>,
85 #[cfg(feature = "logs")]
86 logs_batcher: RwLock<Option<Batcher<Log>>>,
87 #[cfg(feature = "metrics")]
88 metrics_batcher: RwLock<Option<Batcher<Metric>>>,
89 #[cfg(feature = "logs")]
90 default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
91 #[cfg(feature = "metrics")]
92 default_metric_attributes: BTreeMap<Cow<'static, str>, LogAttribute>,
93 integrations: Vec<(TypeId, Arc<dyn Integration>)>,
94 pub(crate) sdk_info: ClientSdkInfo,
95}
96
97impl fmt::Debug for Client {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.debug_struct("Client")
100 .field("dsn", &self.dsn())
101 .field("options", &self.options)
102 .finish()
103 }
104}
105
106impl Clone for Client {
107 fn clone(&self) -> Client {
108 let envelope_sender = self.envelope_sender.clone_with_new_transport_slot();
109
110 #[cfg(feature = "release-health")]
111 let session_flusher = RwLock::new(Some(SessionFlusher::new(
112 envelope_sender.clone(),
113 self.options.session_mode,
114 )));
115
116 #[cfg(feature = "logs")]
117 let logs_batcher = RwLock::new(if self.options.enable_logs {
118 Some(Batcher::new(envelope_sender.clone()))
119 } else {
120 None
121 });
122
123 #[cfg(feature = "metrics")]
124 let metrics_batcher = RwLock::new(
125 self.options
126 .enable_metrics
127 .then(|| Batcher::new(envelope_sender.clone())),
128 );
129
130 Client {
131 options: self.options.clone(),
132 envelope_sender,
133 #[cfg(feature = "release-health")]
134 session_flusher,
135 #[cfg(feature = "logs")]
136 logs_batcher,
137 #[cfg(feature = "metrics")]
138 metrics_batcher,
139 #[cfg(feature = "logs")]
140 default_log_attributes: self.default_log_attributes.clone(),
141 #[cfg(feature = "metrics")]
142 default_metric_attributes: self.default_metric_attributes.clone(),
143 integrations: self.integrations.clone(),
144 sdk_info: self.sdk_info.clone(),
145 }
146 }
147}
148
149impl Client {
150 pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
171 Client::with_options(opts.into())
172 }
173
174 pub fn with_options(mut options: ClientOptions) -> Client {
179 Hub::with_current(|_| {});
182
183 let envelope_sender = build_envelope_sender(&options);
184 let mut sdk_info = SDK_INFO.clone();
185
186 let integrations: Vec<_> = options
189 .integrations
190 .iter()
191 .map(|integration| (integration.as_ref().type_id(), integration.clone()))
192 .collect();
193
194 for (_, integration) in integrations.iter() {
195 integration.setup(&mut options);
196 sdk_info.integrations.push(integration.name().to_string());
197 }
198
199 #[cfg(feature = "release-health")]
200 let session_flusher = RwLock::new(Some(SessionFlusher::new(
201 envelope_sender.clone(),
202 options.session_mode,
203 )));
204
205 #[cfg(feature = "logs")]
206 let logs_batcher = RwLock::new(if options.enable_logs {
207 Some(Batcher::new(envelope_sender.clone()))
208 } else {
209 None
210 });
211
212 #[cfg(feature = "metrics")]
213 let metrics_batcher = RwLock::new(
214 options
215 .enable_metrics
216 .then(|| Batcher::new(envelope_sender.clone())),
217 );
218
219 let client = Client {
220 options,
221 envelope_sender,
222 #[cfg(feature = "release-health")]
223 session_flusher,
224 #[cfg(feature = "logs")]
225 logs_batcher,
226 #[cfg(feature = "metrics")]
227 metrics_batcher,
228 #[cfg(feature = "logs")]
229 default_log_attributes: None,
230 #[cfg(feature = "metrics")]
231 default_metric_attributes: Default::default(),
232 integrations,
233 sdk_info,
234 };
235
236 #[cfg(feature = "logs")]
237 let client = client.with_cached_default_log_attributes();
238
239 #[cfg(feature = "metrics")]
240 let client = client.with_cached_default_metric_attributes();
241
242 client
243 }
244
245 #[cfg(feature = "logs")]
246 fn with_cached_default_log_attributes(mut self) -> Self {
247 let mut attributes = BTreeMap::new();
248
249 if let Some(environment) = self.options.environment.as_ref() {
250 attributes.insert("sentry.environment".to_owned(), environment.clone().into());
251 }
252
253 if let Some(release) = self.options.release.as_ref() {
254 attributes.insert("sentry.release".to_owned(), release.clone().into());
255 }
256
257 attributes.insert(
258 "sentry.sdk.name".to_owned(),
259 self.sdk_info.name.to_owned().into(),
260 );
261
262 attributes.insert(
263 "sentry.sdk.version".to_owned(),
264 self.sdk_info.version.to_owned().into(),
265 );
266
267 let mut fake_event = Event::default();
274 for (_, integration) in self.integrations.iter() {
275 if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
276 fake_event = res;
277 }
278 }
279
280 if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
281 if let Some(name) = os.name.as_ref() {
282 attributes.insert("os.name".to_owned(), name.to_owned().into());
283 }
284 if let Some(version) = os.version.as_ref() {
285 attributes.insert("os.version".to_owned(), version.to_owned().into());
286 }
287 }
288
289 if let Some(server) = &self.options.server_name {
290 attributes.insert("server.address".to_owned(), server.clone().into());
291 }
292
293 self.default_log_attributes = Some(attributes);
294
295 self
296 }
297
298 #[cfg(feature = "metrics")]
299 fn with_cached_default_metric_attributes(mut self) -> Self {
300 let always_present_attributes = [
301 ("sentry.sdk.name", &self.sdk_info.name),
302 ("sentry.sdk.version", &self.sdk_info.version),
303 ]
304 .into_iter()
305 .map(|(name, value)| (name.into(), value.as_str().into()));
306
307 let maybe_present_attributes = [
308 ("sentry.environment", &self.options.environment),
309 ("sentry.release", &self.options.release),
310 ("server.address", &self.options.server_name),
311 ]
312 .into_iter()
313 .filter_map(|(name, value)| value.clone().map(|value| (name.into(), value.into())));
314
315 self.default_metric_attributes = maybe_present_attributes
316 .chain(always_present_attributes)
317 .collect();
318
319 self
320 }
321
322 pub(crate) fn get_integration<I>(&self) -> Option<&I>
323 where
324 I: Integration,
325 {
326 let id = TypeId::of::<I>();
327 let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
328 integration.as_ref().as_any().downcast_ref()
329 }
330
331 pub fn prepare_event(
333 &self,
334 mut event: Event<'static>,
335 scope: Option<&Scope>,
336 ) -> Option<Event<'static>> {
337 if event.event_id.is_nil() {
340 event.event_id = random_uuid();
341 }
342
343 if event.sdk.is_none() {
344 event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
346 }
347
348 if let Some(scope) = scope {
349 event = match scope.apply_to_event(event) {
350 Some(event) => event,
351 None => {
352 self.record_lost_event(ClientReportReason::EventProcessor);
353 return None;
354 }
355 };
356 }
357
358 for (_, integration) in self.integrations.iter() {
359 let id = event.event_id;
360 event = match integration.process_event(event, &self.options) {
361 Some(event) => event,
362 None => {
363 sentry_debug!("integration dropped event {:?}", id);
364 self.record_lost_event(ClientReportReason::EventProcessor);
365 return None;
366 }
367 }
368 }
369
370 if event.release.is_none() {
371 event.release.clone_from(&self.options.release);
372 }
373 if event.environment.is_none() {
374 event.environment.clone_from(&self.options.environment);
375 }
376 if event.server_name.is_none() {
377 event.server_name.clone_from(&self.options.server_name);
378 }
379 if &event.platform == "other" {
380 event.platform = "native".into();
381 }
382
383 if let Some(ref func) = self.options.before_send {
384 sentry_debug!("invoking before_send callback");
385 let id = event.event_id;
386 if let Some(processed_event) = func(event) {
387 event = processed_event;
388 } else {
389 sentry_debug!("before_send dropped event {:?}", id);
390 self.record_lost_event(ClientReportReason::BeforeSend);
391 return None;
392 }
393 }
394
395 #[cfg(feature = "release-health")]
396 if let Some(scope) = scope {
397 scope.update_session_from_event(&event);
398 }
399
400 let sample_rate = event_sample_rate(&self.options.event_sampling_strategy);
401 if !self.sample_should_send(sample_rate) {
402 self.record_lost_event(ClientReportReason::SampleRate);
403 None
404 } else {
405 Some(event)
406 }
407 }
408
409 pub fn options(&self) -> &ClientOptions {
411 &self.options
412 }
413
414 pub fn dsn(&self) -> Option<&Dsn> {
416 self.options.dsn.as_ref()
417 }
418
419 pub fn is_enabled(&self) -> bool {
438 self.options.dsn.is_some() && self.envelope_sender.is_enabled()
439 }
440
441 pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
443 let mut event_id = Default::default();
444 self.envelope_sender.send_envelope_with(|| {
445 self.prepare_event(event, scope).map(|event| {
446 event_id = event.event_id;
447 let mut envelope: Envelope = event.into();
448 #[cfg(feature = "release-health")]
451 if self.options.session_mode == SessionMode::Application {
452 let session_item = scope.and_then(|scope| {
453 scope
454 .session
455 .lock()
456 .unwrap()
457 .as_mut()
458 .and_then(|session| session.create_envelope_item())
459 });
460 if let Some(session_item) = session_item {
461 envelope.add_item(session_item);
462 }
463 }
464
465 if let Some(scope) = scope {
466 for attachment in scope.attachments.iter().cloned() {
467 envelope.add_item(attachment);
468 }
469 }
470
471 envelope
472 })
473 });
474 event_id
475 }
476
477 pub(crate) fn record_lost_data<L>(&self, data: &L, reason: ClientReportReason)
478 where
479 L: LossSource + ?Sized,
480 {
481 self.envelope_sender.record_lost_data(data, reason);
482 }
483
484 fn record_loss(
486 &self,
487 category: ClientReportCategory,
488 reason: ClientReportReason,
489 quantity: u64,
490 ) {
491 self.envelope_sender.record_loss(category, reason, quantity);
492 }
493
494 fn record_lost_event(&self, reason: ClientReportReason) {
496 self.record_loss(ClientReportCategory::Error, reason, 1);
497 }
498
499 pub fn send_envelope(&self, envelope: Envelope) {
501 self.envelope_sender.send_envelope(envelope);
502 }
503
504 #[cfg(feature = "release-health")]
505 pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
506 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
507 flusher.enqueue(session_update);
508 }
509 }
510
511 pub fn flush(&self, timeout: Option<Duration>) -> bool {
513 #[cfg(feature = "release-health")]
514 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
515 flusher.flush();
516 }
517 #[cfg(feature = "logs")]
518 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
519 batcher.flush();
520 }
521 #[cfg(feature = "metrics")]
522 if let Some(ref batcher) = *self.metrics_batcher.read().unwrap() {
523 batcher.flush();
524 }
525 self.envelope_sender
526 .flush(timeout.unwrap_or(self.options.shutdown_timeout))
527 }
528
529 pub fn close(&self, timeout: Option<Duration>) -> bool {
537 #[cfg(feature = "release-health")]
538 drop(self.session_flusher.write().unwrap().take());
539 #[cfg(feature = "logs")]
540 drop(self.logs_batcher.write().unwrap().take());
541 #[cfg(feature = "metrics")]
542 drop(self.metrics_batcher.write().unwrap().take());
543 self.envelope_sender
544 .shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
545 }
546
547 pub fn sample_should_send(&self, rate: f32) -> bool {
550 if rate >= 1.0 {
551 true
552 } else if rate <= 0.0 {
553 false
554 } else {
555 random::<f32>() < rate
556 }
557 }
558
559 #[cfg(feature = "logs")]
561 pub fn capture_log(&self, log: Log, scope: &Scope) {
562 if !self.options.enable_logs {
563 sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
564 return;
565 }
566 if let Some(log) = self.prepare_log(log, scope) {
567 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
568 batcher.enqueue(log);
569 }
570 }
571 }
572
573 #[cfg(feature = "logs")]
576 fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
577 scope.apply_to_log(&mut log);
578
579 if let Some(default_attributes) = self.default_log_attributes.as_ref() {
580 for (key, val) in default_attributes.iter() {
581 log.attributes.entry(key.to_owned()).or_insert(val.clone());
582 }
583 }
584
585 if let Some(ref func) = self.options.before_send_log {
586 let losses: Vec<_> = log.losses().collect();
587 log = match func(log) {
588 Some(log) => log,
589 None => {
590 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
591 return None;
592 }
593 };
594 }
595
596 Some(log)
597 }
598
599 #[cfg(feature = "metrics")]
601 pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
602 if !self.options.enable_metrics {
603 return;
605 }
606
607 if let Some(metric) = self.prepare_metric(metric, scope) {
608 if let Some(batcher) = self
609 .metrics_batcher
610 .read()
611 .expect("metrics batcher lock could not be acquired")
612 .as_ref()
613 {
614 batcher.enqueue(metric);
615 }
616 }
617 }
618
619 #[cfg(feature = "metrics")]
622 fn prepare_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) -> Option<Metric> {
623 let mut metric = scope.apply_to_metric(metric, self.options().send_default_pii);
624
625 for (key, val) in &self.default_metric_attributes {
626 metric.attributes.entry(key.clone()).or_insert(val.clone());
627 }
628
629 if let Some(ref func) = self.options.before_send_metric {
630 let losses: Vec<_> = metric.losses().collect();
631 metric = match func(metric) {
632 Some(metric) => metric,
633 None => {
634 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
635 return None;
636 }
637 };
638 }
639
640 Some(metric)
641 }
642}
643
644impl RefUnwindSafe for Client {}
647
648fn build_envelope_sender(client_options: &ClientOptions) -> EnvelopeSender {
652 let ClientOptions {
653 dsn,
654 transport: transport_factory,
655 user_agent,
656 http_proxy,
657 https_proxy,
658 accept_invalid_certs,
659 ..
660 } = client_options;
661
662 match (dsn.as_ref(), transport_factory.as_ref()) {
663 (Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|client_report_recorder| {
664 let options = TransportOptions {
665 dsn: dsn.clone(),
666 user_agent: user_agent.clone(),
667 http_proxy: http_proxy.clone(),
668 https_proxy: https_proxy.clone(),
669 accept_invalid_certs: *accept_invalid_certs,
670 client_report_recorder,
671 };
672
673 transport_factory.create_transport_with_options(options)
674 }),
675 _ => Default::default(),
676 }
677}