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    /// let client = sentry::Client::from(sentry::ClientOptions::new());
415    /// assert!(!client.is_enabled());
416    ///
417    /// let dsn = "https://public@example.com/1";
418    /// let transport = sentry::test::TestTransport::new();
419    /// let client = sentry::Client::from((
420    ///     dsn,
421    ///     sentry::ClientOptions::new().transport(transport),
422    /// ));
423    /// assert!(client.is_enabled());
424    /// ```
425    pub fn is_enabled(&self) -> bool {
426        self.options.dsn.is_some() && self.envelope_sender.is_enabled()
427    }
428
429    /// Captures an event and sends it to sentry.
430    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                // For request-mode sessions, we aggregate them all instead of
437                // flushing them out early.
438                #[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    /// Records `quantity` lost items for `category` and `reason`.
473    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    /// Records one lost error event for `reason`.
483    fn record_lost_event(&self, reason: ClientReportReason) {
484        self.record_loss(ClientReportCategory::Error, reason, 1);
485    }
486
487    /// Sends the specified [`Envelope`] to sentry.
488    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    /// Drains all pending events without shutting down.
500    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    /// Drains all pending events and shuts down the transport behind the
518    /// client.  After shutting down the transport is removed.
519    ///
520    /// This returns `true` if the queue was successfully drained in the
521    /// given time or `false` if not (for instance because of a timeout).
522    /// If no timeout is provided the client will wait for as long a
523    /// `shutdown_timeout` in the client options.
524    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    /// Returns a random boolean with a probability defined
536    /// by rate
537    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    /// Captures a log and sends it to Sentry.
548    #[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    /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
562    /// processing it through `before_send_log`.
563    #[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    /// Captures a metric and sends it to Sentry.
588    #[cfg(feature = "metrics")]
589    pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
590        if !self.options.enable_metrics {
591            // Skip preparing the metric if we don't send it anyways.
592            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    /// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
608    /// processing it through `before_send_metric`.
609    #[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
632// Make this unwind safe. It's not out of the box because of the
633// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
634impl RefUnwindSafe for Client {}
635
636/// Build an [`EnvelopeSender`] from the given [`ClientOptions`].
637///
638/// If either the `dsn` or the `transport` are `None`, a no-op [`EnvelopeSender`] is returned.
639fn 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}