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(Some(Batcher::new(envelope_sender.clone())));
118
119 #[cfg(feature = "metrics")]
120 let metrics_batcher = RwLock::new(Some(Batcher::new(envelope_sender.clone())));
121
122 Client {
123 options: self.options.clone(),
124 envelope_sender,
125 #[cfg(feature = "release-health")]
126 session_flusher,
127 #[cfg(feature = "logs")]
128 logs_batcher,
129 #[cfg(feature = "metrics")]
130 metrics_batcher,
131 #[cfg(feature = "logs")]
132 default_log_attributes: self.default_log_attributes.clone(),
133 #[cfg(feature = "metrics")]
134 default_metric_attributes: self.default_metric_attributes.clone(),
135 integrations: self.integrations.clone(),
136 sdk_info: self.sdk_info.clone(),
137 }
138 }
139}
140
141impl Client {
142 pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
163 Client::with_options(opts.into())
164 }
165
166 pub fn with_options(mut options: ClientOptions) -> Client {
171 Hub::with_current(|_| {});
174
175 let envelope_sender = build_envelope_sender(&options);
176 let mut sdk_info = SDK_INFO.clone();
177
178 let integrations: Vec<_> = options
181 .integrations
182 .iter()
183 .map(|integration| (integration.as_ref().type_id(), integration.clone()))
184 .collect();
185
186 for (_, integration) in integrations.iter() {
187 integration.setup(&mut options);
188 sdk_info.integrations.push(integration.name().to_string());
189 }
190
191 #[cfg(feature = "release-health")]
192 let session_flusher = RwLock::new(Some(SessionFlusher::new(
193 envelope_sender.clone(),
194 options.session_mode,
195 )));
196
197 #[cfg(feature = "logs")]
198 let logs_batcher = RwLock::new(Some(Batcher::new(envelope_sender.clone())));
199
200 #[cfg(feature = "metrics")]
201 let metrics_batcher = RwLock::new(Some(Batcher::new(envelope_sender.clone())));
202
203 let client = Client {
204 options,
205 envelope_sender,
206 #[cfg(feature = "release-health")]
207 session_flusher,
208 #[cfg(feature = "logs")]
209 logs_batcher,
210 #[cfg(feature = "metrics")]
211 metrics_batcher,
212 #[cfg(feature = "logs")]
213 default_log_attributes: None,
214 #[cfg(feature = "metrics")]
215 default_metric_attributes: Default::default(),
216 integrations,
217 sdk_info,
218 };
219
220 #[cfg(feature = "logs")]
221 let client = client.with_cached_default_log_attributes();
222
223 #[cfg(feature = "metrics")]
224 let client = client.with_cached_default_metric_attributes();
225
226 client
227 }
228
229 #[cfg(feature = "logs")]
230 fn with_cached_default_log_attributes(mut self) -> Self {
231 let mut attributes = BTreeMap::new();
232
233 if let Some(environment) = self.options.environment.as_ref() {
234 attributes.insert("sentry.environment".to_owned(), environment.clone().into());
235 }
236
237 if let Some(release) = self.options.release.as_ref() {
238 attributes.insert("sentry.release".to_owned(), release.clone().into());
239 }
240
241 attributes.insert(
242 "sentry.sdk.name".to_owned(),
243 self.sdk_info.name.to_owned().into(),
244 );
245
246 attributes.insert(
247 "sentry.sdk.version".to_owned(),
248 self.sdk_info.version.to_owned().into(),
249 );
250
251 let mut fake_event = Event::default();
258 for (_, integration) in self.integrations.iter() {
259 if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
260 fake_event = res;
261 }
262 }
263
264 if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
265 if let Some(name) = os.name.as_ref() {
266 attributes.insert("os.name".to_owned(), name.to_owned().into());
267 }
268 if let Some(version) = os.version.as_ref() {
269 attributes.insert("os.version".to_owned(), version.to_owned().into());
270 }
271 }
272
273 if let Some(server) = &self.options.server_name {
274 attributes.insert("server.address".to_owned(), server.clone().into());
275 }
276
277 self.default_log_attributes = Some(attributes);
278
279 self
280 }
281
282 #[cfg(feature = "metrics")]
283 fn with_cached_default_metric_attributes(mut self) -> Self {
284 let always_present_attributes = [
285 ("sentry.sdk.name", &self.sdk_info.name),
286 ("sentry.sdk.version", &self.sdk_info.version),
287 ]
288 .into_iter()
289 .map(|(name, value)| (name.into(), value.as_str().into()));
290
291 let maybe_present_attributes = [
292 ("sentry.environment", &self.options.environment),
293 ("sentry.release", &self.options.release),
294 ("server.address", &self.options.server_name),
295 ]
296 .into_iter()
297 .filter_map(|(name, value)| value.clone().map(|value| (name.into(), value.into())));
298
299 self.default_metric_attributes = maybe_present_attributes
300 .chain(always_present_attributes)
301 .collect();
302
303 self
304 }
305
306 pub(crate) fn get_integration<I>(&self) -> Option<&I>
307 where
308 I: Integration,
309 {
310 let id = TypeId::of::<I>();
311 let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
312 integration.as_ref().as_any().downcast_ref()
313 }
314
315 pub fn prepare_event(
317 &self,
318 mut event: Event<'static>,
319 scope: Option<&Scope>,
320 ) -> Option<Event<'static>> {
321 if event.event_id.is_nil() {
324 event.event_id = random_uuid();
325 }
326
327 if event.sdk.is_none() {
328 event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
330 }
331
332 if let Some(scope) = scope {
333 event = match scope.apply_to_event(event) {
334 Some(event) => event,
335 None => {
336 self.record_lost_event(ClientReportReason::EventProcessor);
337 return None;
338 }
339 };
340 }
341
342 for (_, integration) in self.integrations.iter() {
343 let id = event.event_id;
344 event = match integration.process_event(event, &self.options) {
345 Some(event) => event,
346 None => {
347 sentry_debug!("integration dropped event {:?}", id);
348 self.record_lost_event(ClientReportReason::EventProcessor);
349 return None;
350 }
351 }
352 }
353
354 if event.release.is_none() {
355 event.release.clone_from(&self.options.release);
356 }
357 if event.environment.is_none() {
358 event.environment.clone_from(&self.options.environment);
359 }
360 if event.server_name.is_none() {
361 event.server_name.clone_from(&self.options.server_name);
362 }
363 if &event.platform == "other" {
364 event.platform = "native".into();
365 }
366
367 if let Some(ref func) = self.options.before_send {
368 sentry_debug!("invoking before_send callback");
369 let id = event.event_id;
370 if let Some(processed_event) = func(event) {
371 event = processed_event;
372 } else {
373 sentry_debug!("before_send dropped event {:?}", id);
374 self.record_lost_event(ClientReportReason::BeforeSend);
375 return None;
376 }
377 }
378
379 #[cfg(feature = "release-health")]
380 if let Some(scope) = scope {
381 scope.update_session_from_event(&event);
382 }
383
384 let sample_rate = event_sample_rate(&self.options.event_sampling_strategy);
385 if !self.sample_should_send(sample_rate) {
386 self.record_lost_event(ClientReportReason::SampleRate);
387 None
388 } else {
389 Some(event)
390 }
391 }
392
393 pub fn options(&self) -> &ClientOptions {
395 &self.options
396 }
397
398 pub fn dsn(&self) -> Option<&Dsn> {
400 self.options.dsn.as_ref()
401 }
402
403 pub fn is_enabled(&self) -> bool {
422 self.options.dsn.is_some() && self.envelope_sender.is_enabled()
423 }
424
425 pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
427 let mut event_id = Default::default();
428 self.envelope_sender.send_envelope_with(|| {
429 self.prepare_event(event, scope).map(|event| {
430 event_id = event.event_id;
431 let mut envelope: Envelope = event.into();
432 #[cfg(feature = "release-health")]
435 if self.options.session_mode == SessionMode::Application {
436 let session_item = scope.and_then(|scope| {
437 scope
438 .session
439 .lock()
440 .unwrap()
441 .as_mut()
442 .and_then(|session| session.create_envelope_item())
443 });
444 if let Some(session_item) = session_item {
445 envelope.add_item(session_item);
446 }
447 }
448
449 if let Some(scope) = scope {
450 for attachment in scope.attachments.iter().cloned() {
451 envelope.add_item(attachment);
452 }
453 }
454
455 envelope
456 })
457 });
458 event_id
459 }
460
461 pub(crate) fn record_lost_data<L>(&self, data: &L, reason: ClientReportReason)
462 where
463 L: LossSource + ?Sized,
464 {
465 self.envelope_sender.record_lost_data(data, reason);
466 }
467
468 fn record_loss(
470 &self,
471 category: ClientReportCategory,
472 reason: ClientReportReason,
473 quantity: u64,
474 ) {
475 self.envelope_sender.record_loss(category, reason, quantity);
476 }
477
478 fn record_lost_event(&self, reason: ClientReportReason) {
480 self.record_loss(ClientReportCategory::Error, reason, 1);
481 }
482
483 pub fn send_envelope(&self, envelope: Envelope) {
485 self.envelope_sender.send_envelope(envelope);
486 }
487
488 #[cfg(feature = "release-health")]
489 pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
490 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
491 flusher.enqueue(session_update);
492 }
493 }
494
495 pub fn flush(&self, timeout: Option<Duration>) -> bool {
497 #[cfg(feature = "release-health")]
498 if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
499 flusher.flush();
500 }
501 #[cfg(feature = "logs")]
502 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
503 batcher.flush();
504 }
505 #[cfg(feature = "metrics")]
506 if let Some(ref batcher) = *self.metrics_batcher.read().unwrap() {
507 batcher.flush();
508 }
509 self.envelope_sender
510 .flush(timeout.unwrap_or(self.options.shutdown_timeout))
511 }
512
513 pub fn close(&self, timeout: Option<Duration>) -> bool {
521 #[cfg(feature = "release-health")]
522 drop(self.session_flusher.write().unwrap().take());
523 #[cfg(feature = "logs")]
524 drop(self.logs_batcher.write().unwrap().take());
525 #[cfg(feature = "metrics")]
526 drop(self.metrics_batcher.write().unwrap().take());
527 self.envelope_sender
528 .shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
529 }
530
531 pub fn sample_should_send(&self, rate: f32) -> bool {
534 if rate >= 1.0 {
535 true
536 } else if rate <= 0.0 {
537 false
538 } else {
539 random::<f32>() < rate
540 }
541 }
542
543 #[cfg(feature = "logs")]
545 pub fn capture_log(&self, log: Log, scope: &Scope) {
546 if let Some(log) = self.prepare_log(log, scope) {
547 if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
548 batcher.enqueue(log);
549 }
550 }
551 }
552
553 #[cfg(feature = "logs")]
556 fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
557 scope.apply_to_log(&mut log);
558
559 if let Some(default_attributes) = self.default_log_attributes.as_ref() {
560 for (key, val) in default_attributes.iter() {
561 log.attributes.entry(key.to_owned()).or_insert(val.clone());
562 }
563 }
564
565 if let Some(ref func) = self.options.before_send_log {
566 let losses: Vec<_> = log.losses().collect();
567 log = match func(log) {
568 Some(log) => log,
569 None => {
570 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
571 return None;
572 }
573 };
574 }
575
576 Some(log)
577 }
578
579 #[cfg(feature = "metrics")]
581 pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
582 if let Some(metric) = self.prepare_metric(metric, scope) {
583 if let Some(batcher) = self
584 .metrics_batcher
585 .read()
586 .expect("metrics batcher lock could not be acquired")
587 .as_ref()
588 {
589 batcher.enqueue(metric);
590 }
591 }
592 }
593
594 #[cfg(feature = "metrics")]
597 fn prepare_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) -> Option<Metric> {
598 let mut metric = scope.apply_to_metric(metric, self.options().send_default_pii);
599
600 for (key, val) in &self.default_metric_attributes {
601 metric.attributes.entry(key.clone()).or_insert(val.clone());
602 }
603
604 if let Some(ref func) = self.options.before_send_metric {
605 let losses: Vec<_> = metric.losses().collect();
606 metric = match func(metric) {
607 Some(metric) => metric,
608 None => {
609 self.record_lost_data(losses.as_slice(), ClientReportReason::BeforeSend);
610 return None;
611 }
612 };
613 }
614
615 Some(metric)
616 }
617}
618
619impl RefUnwindSafe for Client {}
622
623fn build_envelope_sender(client_options: &ClientOptions) -> EnvelopeSender {
627 let ClientOptions {
628 dsn,
629 transport: transport_factory,
630 user_agent,
631 http_proxy,
632 https_proxy,
633 accept_invalid_certs,
634 ..
635 } = client_options;
636
637 match (dsn.as_ref(), transport_factory.as_ref()) {
638 (Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|client_report_recorder| {
639 let options = TransportOptions {
640 dsn: dsn.clone(),
641 user_agent: user_agent.clone(),
642 http_proxy: http_proxy.clone(),
643 https_proxy: https_proxy.clone(),
644 accept_invalid_certs: *accept_invalid_certs,
645 client_report_recorder,
646 };
647
648 transport_factory.create_transport_with_options(options)
649 }),
650 _ => Default::default(),
651 }
652}