Skip to main content

sentry_core/client/
mod.rs

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
56/// The Sentry Client.
57///
58/// The Client is responsible for event processing and sending events to the
59/// sentry server via the configured [`Transport`]. It can be created from a
60/// [`ClientOptions`].
61///
62/// See the [Unified API] document for more details.
63///
64/// # Examples
65///
66/// ```
67/// sentry::Client::from(sentry::ClientOptions::default());
68/// ```
69///
70/// [`ClientOptions`]: struct.ClientOptions.html
71/// [`Transport`]: trait.Transport.html
72/// [Unified API]: https://develop.sentry.dev/sdk/unified-api/
73pub 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    /// Creates a new Sentry client from a config.
144    ///
145    /// # Supported Configs
146    ///
147    /// The following common values are supported for the client config:
148    ///
149    /// * `ClientOptions`: configure the client with the given client options.
150    /// * `()` or empty string: Disable the client.
151    /// * `&str` / `String` / `&OsStr` / `String`: configure the client with the given DSN.
152    /// * `Dsn` / `&Dsn`: configure the client with a given DSN.
153    /// * `(Dsn, ClientOptions)`: configure the client from the given DSN and optional options.
154    ///
155    /// The `Default` implementation of `ClientOptions` pulls in the DSN from the
156    /// `SENTRY_DSN` environment variable.
157    ///
158    /// # Panics
159    ///
160    /// The `Into<ClientOptions>` implementations can panic for the forms where a DSN needs to be
161    /// parsed.  If you want to handle invalid DSNs you need to parse them manually by calling
162    /// parse on it and handle the error.
163    pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
164        Client::with_options(opts.into())
165    }
166
167    /// Creates a new sentry client for the given options.
168    ///
169    /// If the DSN on the options is set to `None` the client will be entirely
170    /// disabled.
171    pub fn with_options(mut options: ClientOptions) -> Client {
172        // Create the main hub eagerly to avoid problems with the background thread
173        // See https://github.com/getsentry/sentry-rust/issues/237
174        Hub::with_current(|_| {});
175
176        let envelope_sender = build_envelope_sender(&options);
177        let mut sdk_info = SDK_INFO.clone();
178
179        // NOTE: We do not filter out duplicate integrations based on their
180        // TypeId.
181        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        // Process a fake event through integrations, so that `ContextIntegration` (if available)
262        // provides the OS Context.
263        // This is needed as that integration adds the OS Context to events using an event
264        // processor, which logs don't go through.
265        // We cannot get the `ContextIntegration` directly, as its type lives in `sentry-contexts`,
266        // which `sentry-core` doesn't depend on.
267        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    /// Prepares an event for transmission to sentry.
322    pub fn prepare_event(
323        &self,
324        mut event: Event<'static>,
325        scope: Option<&Scope>,
326    ) -> Option<Event<'static>> {
327        // event_id and sdk_info are set before the processors run so that the
328        // processors can poke around in that data.
329        if event.event_id.is_nil() {
330            event.event_id = random_uuid();
331        }
332
333        if event.sdk.is_none() {
334            // NOTE: we need to clone here because `Event` must be `'static`
335            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    /// Returns the options of this client.
398    pub fn options(&self) -> &ClientOptions {
399        &self.options
400    }
401
402    /// Returns the DSN that constructed this client.
403    pub fn dsn(&self) -> Option<&Dsn> {
404        self.options.dsn.as_ref()
405    }
406
407    /// Quick check to see if the client is enabled.
408    ///
409    /// The Client is enabled if it has a valid DSN and Transport configured.
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// use std::sync::Arc;
415    ///
416    /// let client = sentry::Client::from(sentry::ClientOptions::default());
417    /// assert!(!client.is_enabled());
418    ///
419    /// let dsn = "https://public@example.com/1";
420    /// let transport = sentry::test::TestTransport::new();
421    /// let client = sentry::Client::from((
422    ///     dsn,
423    ///     sentry::ClientOptions {
424    ///         transport: Some(Arc::new(transport)),
425    ///         ..Default::default()
426    ///     },
427    /// ));
428    /// assert!(client.is_enabled());
429    /// ```
430    pub fn is_enabled(&self) -> bool {
431        self.options.dsn.is_some() && self.envelope_sender.is_enabled()
432    }
433
434    /// Captures an event and sends it to sentry.
435    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                // For request-mode sessions, we aggregate them all instead of
442                // flushing them out early.
443                #[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    /// Records `quantity` lost items for `category` and `reason`.
478    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    /// Records one lost error event for `reason`.
488    fn record_lost_event(&self, reason: ClientReportReason) {
489        self.record_loss(ClientReportCategory::Error, reason, 1);
490    }
491
492    /// Sends the specified [`Envelope`] to sentry.
493    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    /// Drains all pending events without shutting down.
505    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    /// Drains all pending events and shuts down the transport behind the
523    /// client.  After shutting down the transport is removed.
524    ///
525    /// This returns `true` if the queue was successfully drained in the
526    /// given time or `false` if not (for instance because of a timeout).
527    /// If no timeout is provided the client will wait for as long a
528    /// `shutdown_timeout` in the client options.
529    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    /// Returns a random boolean with a probability defined
541    /// by rate
542    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    /// Captures a log and sends it to Sentry.
553    #[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    /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
567    /// processing it through `before_send_log`.
568    #[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    /// Captures a metric and sends it to Sentry.
593    #[cfg(feature = "metrics")]
594    pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
595        if !self.options.enable_metrics {
596            // Skip preparing the metric if we don't send it anyways.
597            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    /// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
613    /// processing it through `before_send_metric`.
614    #[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
637// Make this unwind safe. It's not out of the box because of the
638// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
639impl RefUnwindSafe for Client {}
640
641/// Build an [`EnvelopeSender`] from the given [`ClientOptions`].
642///
643/// If either the `dsn` or the `transport` are `None`, a no-op [`EnvelopeSender`] is returned.
644fn 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}