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 {
426 self.options.dsn.is_some() && self.envelope_sender.is_enabled()
427 }
428
429 pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
431 let mut event_id = Default::default();
432 self.envelope_sender.send_envelope_with(|| {
433 self.prepare_event(event, scope).map(|event| {
434 event_id = event.event_id;
435 let mut envelope: Envelope = event.into();
436 #[cfg(feature = "release-health")]
439 if self.options.session_mode == SessionMode::Application {
440 let session_item = scope.and_then(|scope| {
441 scope
442 .session
443 .lock()
444 .unwrap()
445 .as_mut()
446 .and_then(|session| session.create_envelope_item())
447 });
448 if let Some(session_item) = session_item {
449 envelope.add_item(session_item);
450 }
451 }
452
453 if let Some(scope) = scope {
454 for attachment in scope.attachments.iter().cloned() {
455 envelope.add_item(attachment);
456 }
457 }
458
459 envelope
460 })
461 });
462 event_id
463 }
464
465 pub(crate) fn record_lost_data<L>(&self, data: &L, reason: ClientReportReason)
466 where
467 L: LossSource + ?Sized,
468 {
469 self.envelope_sender.record_lost_data(data, reason);
470 }
471
472 fn record_loss(
474 &self,
475 category: ClientReportCategory,
476 reason: ClientReportReason,
477 quantity: u64,
478 ) {
479 self.envelope_sender.record_loss(category, reason, quantity);
480 }
481
482 fn record_lost_event(&self, reason: ClientReportReason) {
484 self.record_loss(ClientReportCategory::Error, reason, 1);
485 }
486
487 pub fn send_envelope(&self, envelope: Envelope) {
489 self.envelope_sender.send_envelope(envelope);
490 }
491
492 #[cfg(feature = "release-health")]
493 pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
494 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
495 flusher.enqueue(session_update);
496 }
497 }
498
499 pub fn flush(&self, timeout: Option<Duration>) -> bool {
501 #[cfg(feature = "release-health")]
502 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
503 flusher.flush();
504 }
505 #[cfg(feature = "logs")]
506 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
507 batcher.flush();
508 }
509 #[cfg(feature = "metrics")]
510 if let Some(ref batcher) = *self.metrics_batcher.read().unwrap() {
511 batcher.flush();
512 }
513 self.envelope_sender
514 .flush(timeout.unwrap_or(self.options.shutdown_timeout))
515 }
516
517 pub fn close(&self, timeout: Option<Duration>) -> bool {
525 #[cfg(feature = "release-health")]
526 drop(self.session_flusher.write().unwrap().take());
527 #[cfg(feature = "logs")]
528 drop(self.logs_batcher.write().unwrap().take());
529 #[cfg(feature = "metrics")]
530 drop(self.metrics_batcher.write().unwrap().take());
531 self.envelope_sender
532 .shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
533 }
534
535 pub fn sample_should_send(&self, rate: f32) -> bool {
538 if rate >= 1.0 {
539 true
540 } else if rate <= 0.0 {
541 false
542 } else {
543 random::<f32>() < rate
544 }
545 }
546
547 #[cfg(feature = "logs")]
549 pub fn capture_log(&self, log: Log, scope: &Scope) {
550 if !self.options.enable_logs {
551 sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
552 return;
553 }
554 if let Some(log) = self.prepare_log(log, scope) {
555 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
556 batcher.enqueue(log);
557 }
558 }
559 }
560
561 #[cfg(feature = "logs")]
564 fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
565 scope.apply_to_log(&mut log);
566
567 if let Some(default_attributes) = self.default_log_attributes.as_ref() {
568 for (key, val) in default_attributes.iter() {
569 log.attributes.entry(key.to_owned()).or_insert(val.clone());
570 }
571 }
572
573 if let Some(ref func) = self.options.before_send_log {
574 let losses: Vec<_> = log.losses().collect();
575 log = match func(log) {
576 Some(log) => log,
577 None => {
578 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
579 return None;
580 }
581 };
582 }
583
584 Some(log)
585 }
586
587 #[cfg(feature = "metrics")]
589 pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
590 if !self.options.enable_metrics {
591 return;
593 }
594
595 if let Some(metric) = self.prepare_metric(metric, scope) {
596 if let Some(batcher) = self
597 .metrics_batcher
598 .read()
599 .expect("metrics batcher lock could not be acquired")
600 .as_ref()
601 {
602 batcher.enqueue(metric);
603 }
604 }
605 }
606
607 #[cfg(feature = "metrics")]
610 fn prepare_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) -> Option<Metric> {
611 let mut metric = scope.apply_to_metric(metric, self.options().send_default_pii);
612
613 for (key, val) in &self.default_metric_attributes {
614 metric.attributes.entry(key.clone()).or_insert(val.clone());
615 }
616
617 if let Some(ref func) = self.options.before_send_metric {
618 let losses: Vec<_> = metric.losses().collect();
619 metric = match func(metric) {
620 Some(metric) => metric,
621 None => {
622 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
623 return None;
624 }
625 };
626 }
627
628 Some(metric)
629 }
630}
631
632impl RefUnwindSafe for Client {}
635
636fn build_envelope_sender(client_options: &ClientOptions) -> EnvelopeSender {
640 let ClientOptions {
641 dsn,
642 transport: transport_factory,
643 user_agent,
644 http_proxy,
645 https_proxy,
646 accept_invalid_certs,
647 ..
648 } = client_options;
649
650 match (dsn.as_ref(), transport_factory.as_ref()) {
651 (Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|client_report_recorder| {
652 let options = TransportOptions {
653 dsn: dsn.clone(),
654 user_agent: user_agent.clone(),
655 http_proxy: http_proxy.clone(),
656 https_proxy: https_proxy.clone(),
657 accept_invalid_certs: *accept_invalid_certs,
658 client_report_recorder,
659 };
660
661 transport_factory.create_transport_with_options(options)
662 }),
663 _ => Default::default(),
664 }
665}