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, 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
50/// Functional implementation of how an event's sample rate is chosen.
51fn 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
63/// The Sentry Client.
64///
65/// The Client is responsible for event processing and sending events to the
66/// sentry server via the configured [`Transport`]. It can be created from a
67/// [`ClientOptions`].
68///
69/// See the [Unified API] document for more details.
70///
71/// # Examples
72///
73/// ```
74/// sentry::Client::from(sentry::ClientOptions::default());
75/// ```
76///
77/// [`ClientOptions`]: struct.ClientOptions.html
78/// [`Transport`]: trait.Transport.html
79/// [Unified API]: https://develop.sentry.dev/sdk/unified-api/
80pub 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    /// Creates a new Sentry client from a config.
151    ///
152    /// # Supported Configs
153    ///
154    /// The following common values are supported for the client config:
155    ///
156    /// * `ClientOptions`: configure the client with the given client options.
157    /// * `()` or empty string: Disable the client.
158    /// * `&str` / `String` / `&OsStr` / `String`: configure the client with the given DSN.
159    /// * `Dsn` / `&Dsn`: configure the client with a given DSN.
160    /// * `(Dsn, ClientOptions)`: configure the client from the given DSN and optional options.
161    ///
162    /// The `Default` implementation of `ClientOptions` pulls in the DSN from the
163    /// `SENTRY_DSN` environment variable.
164    ///
165    /// # Panics
166    ///
167    /// The `Into<ClientOptions>` implementations can panic for the forms where a DSN needs to be
168    /// parsed.  If you want to handle invalid DSNs you need to parse them manually by calling
169    /// parse on it and handle the error.
170    pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
171        Client::with_options(opts.into())
172    }
173
174    /// Creates a new sentry client for the given options.
175    ///
176    /// If the DSN on the options is set to `None` the client will be entirely
177    /// disabled.
178    pub fn with_options(mut options: ClientOptions) -> Client {
179        // Create the main hub eagerly to avoid problems with the background thread
180        // See https://github.com/getsentry/sentry-rust/issues/237
181        Hub::with_current(|_| {});
182
183        let envelope_sender = build_envelope_sender(&options);
184        let mut sdk_info = SDK_INFO.clone();
185
186        // NOTE: We do not filter out duplicate integrations based on their
187        // TypeId.
188        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        // Process a fake event through integrations, so that `ContextIntegration` (if available)
268        // provides the OS Context.
269        // This is needed as that integration adds the OS Context to events using an event
270        // processor, which logs don't go through.
271        // We cannot get the `ContextIntegration` directly, as its type lives in `sentry-contexts`,
272        // which `sentry-core` doesn't depend on.
273        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    /// Prepares an event for transmission to sentry.
332    pub fn prepare_event(
333        &self,
334        mut event: Event<'static>,
335        scope: Option<&Scope>,
336    ) -> Option<Event<'static>> {
337        // event_id and sdk_info are set before the processors run so that the
338        // processors can poke around in that data.
339        if event.event_id.is_nil() {
340            event.event_id = random_uuid();
341        }
342
343        if event.sdk.is_none() {
344            // NOTE: we need to clone here because `Event` must be `'static`
345            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    /// Returns the options of this client.
410    pub fn options(&self) -> &ClientOptions {
411        &self.options
412    }
413
414    /// Returns the DSN that constructed this client.
415    pub fn dsn(&self) -> Option<&Dsn> {
416        self.options.dsn.as_ref()
417    }
418
419    /// Quick check to see if the client is enabled.
420    ///
421    /// The Client is enabled if it has a valid DSN and Transport configured.
422    ///
423    /// # Examples
424    ///
425    /// ```
426    /// let client = sentry::Client::from(sentry::ClientOptions::new());
427    /// assert!(!client.is_enabled());
428    ///
429    /// let dsn = "https://public@example.com/1";
430    /// let transport = sentry::test::TestTransport::new();
431    /// let client = sentry::Client::from((
432    ///     dsn,
433    ///     sentry::ClientOptions::new().transport(transport),
434    /// ));
435    /// assert!(client.is_enabled());
436    /// ```
437    pub fn is_enabled(&self) -> bool {
438        self.options.dsn.is_some() && self.envelope_sender.is_enabled()
439    }
440
441    /// Captures an event and sends it to sentry.
442    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                // For request-mode sessions, we aggregate them all instead of
449                // flushing them out early.
450                #[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    /// Records `quantity` lost items for `category` and `reason`.
485    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    /// Records one lost error event for `reason`.
495    fn record_lost_event(&self, reason: ClientReportReason) {
496        self.record_loss(ClientReportCategory::Error, reason, 1);
497    }
498
499    /// Sends the specified [`Envelope`] to sentry.
500    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    /// Drains all pending events without shutting down.
512    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    /// Drains all pending events and shuts down the transport behind the
530    /// client.  After shutting down the transport is removed.
531    ///
532    /// This returns `true` if the queue was successfully drained in the
533    /// given time or `false` if not (for instance because of a timeout).
534    /// If no timeout is provided the client will wait for as long a
535    /// `shutdown_timeout` in the client options.
536    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    /// Returns a random boolean with a probability defined
548    /// by rate
549    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    /// Captures a log and sends it to Sentry.
560    #[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    /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
574    /// processing it through `before_send_log`.
575    #[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    /// Captures a metric and sends it to Sentry.
600    #[cfg(feature = "metrics")]
601    pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M, scope: &Scope) {
602        if !self.options.enable_metrics {
603            // Skip preparing the metric if we don't send it anyways.
604            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    /// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
620    /// processing it through `before_send_metric`.
621    #[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
644// Make this unwind safe. It's not out of the box because of the
645// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
646impl RefUnwindSafe for Client {}
647
648/// Build an [`EnvelopeSender`] from the given [`ClientOptions`].
649///
650/// If either the `dsn` or the `transport` are `None`, a no-op [`EnvelopeSender`] is returned.
651fn 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}