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(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    /// Creates a new Sentry client from a config.
143    ///
144    /// # Supported Configs
145    ///
146    /// The following common values are supported for the client config:
147    ///
148    /// * `ClientOptions`: configure the client with the given client options.
149    /// * `()` or empty string: Disable the client.
150    /// * `&str` / `String` / `&OsStr` / `String`: configure the client with the given DSN.
151    /// * `Dsn` / `&Dsn`: configure the client with a given DSN.
152    /// * `(Dsn, ClientOptions)`: configure the client from the given DSN and optional options.
153    ///
154    /// The `Default` implementation of `ClientOptions` pulls in the DSN from the
155    /// `SENTRY_DSN` environment variable.
156    ///
157    /// # Panics
158    ///
159    /// The `Into<ClientOptions>` implementations can panic for the forms where a DSN needs to be
160    /// parsed.  If you want to handle invalid DSNs you need to parse them manually by calling
161    /// parse on it and handle the error.
162    pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
163        Client::with_options(opts.into())
164    }
165
166    /// Creates a new sentry client for the given options.
167    ///
168    /// If the DSN on the options is set to `None` the client will be entirely
169    /// disabled.
170    pub fn with_options(mut options: ClientOptions) -> Client {
171        // Create the main hub eagerly to avoid problems with the background thread
172        // See https://github.com/getsentry/sentry-rust/issues/237
173        Hub::with_current(|_| {});
174
175        let envelope_sender = build_envelope_sender(&options);
176        let mut sdk_info = SDK_INFO.clone();
177
178        // NOTE: We do not filter out duplicate integrations based on their
179        // TypeId.
180        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        // Process a fake event through integrations, so that `ContextIntegration` (if available)
252        // provides the OS Context.
253        // This is needed as that integration adds the OS Context to events using an event
254        // processor, which logs don't go through.
255        // We cannot get the `ContextIntegration` directly, as its type lives in `sentry-contexts`,
256        // which `sentry-core` doesn't depend on.
257        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    /// Prepares an event for transmission to sentry.
316    pub fn prepare_event(
317        &self,
318        mut event: Event<'static>,
319        scope: Option<&Scope>,
320    ) -> Option<Event<'static>> {
321        // event_id and sdk_info are set before the processors run so that the
322        // processors can poke around in that data.
323        if event.event_id.is_nil() {
324            event.event_id = random_uuid();
325        }
326
327        if event.sdk.is_none() {
328            // NOTE: we need to clone here because `Event` must be `'static`
329            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    /// Returns the options of this client.
394    pub fn options(&self) -> &ClientOptions {
395        &self.options
396    }
397
398    /// Returns the DSN that constructed this client.
399    pub fn dsn(&self) -> Option<&Dsn> {
400        self.options.dsn.as_ref()
401    }
402
403    /// Quick check to see if the client is enabled.
404    ///
405    /// The Client is enabled if it has a valid DSN and Transport configured.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// let client = sentry::Client::from(sentry::ClientOptions::new());
411    /// assert!(!client.is_enabled());
412    ///
413    /// let dsn = "https://public@example.com/1";
414    /// let transport = sentry::test::TestTransport::new();
415    /// let client = sentry::Client::from((
416    ///     dsn,
417    ///     sentry::ClientOptions::new().transport(transport),
418    /// ));
419    /// assert!(client.is_enabled());
420    /// ```
421    pub fn is_enabled(&self) -> bool {
422        self.options.dsn.is_some() && self.envelope_sender.is_enabled()
423    }
424
425    /// Captures an event and sends it to sentry.
426    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                // For request-mode sessions, we aggregate them all instead of
433                // flushing them out early.
434                #[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    /// Records `quantity` lost items for `category` and `reason`.
469    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    /// Records one lost error event for `reason`.
479    fn record_lost_event(&self, reason: ClientReportReason) {
480        self.record_loss(ClientReportCategory::Error, reason, 1);
481    }
482
483    /// Sends the specified [`Envelope`] to sentry.
484    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    /// Drains all pending events without shutting down.
496    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    /// Drains all pending events and shuts down the transport behind the
514    /// client.  After shutting down the transport is removed.
515    ///
516    /// This returns `true` if the queue was successfully drained in the
517    /// given time or `false` if not (for instance because of a timeout).
518    /// If no timeout is provided the client will wait for as long a
519    /// `shutdown_timeout` in the client options.
520    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    /// Returns a random boolean with a probability defined
532    /// by rate
533    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    /// Captures a log and sends it to Sentry.
544    #[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    /// Prepares a log to be sent, setting the `trace_id` and other default attributes, and
554    /// processing it through `before_send_log`.
555    #[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    /// Captures a metric and sends it to Sentry.
580    #[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    /// Prepares a metric to be sent, setting the `trace_id` and other default attributes, and
595    /// processing it through `before_send_metric`.
596    #[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
619// Make this unwind safe. It's not out of the box because of the
620// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
621impl RefUnwindSafe for Client {}
622
623/// Build an [`EnvelopeSender`] from the given [`ClientOptions`].
624///
625/// If either the `dsn` or the `transport` are `None`, a no-op [`EnvelopeSender`] is returned.
626fn 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}