sentry_core/
client.rs

1use std::any::TypeId;
2use std::borrow::Cow;
3use std::fmt;
4use std::panic::RefUnwindSafe;
5use std::sync::{Arc, RwLock};
6use std::time::Duration;
7
8use rand::random;
9use sentry_types::protocol::v7::SessionUpdate;
10use sentry_types::random_uuid;
11
12use crate::constants::SDK_INFO;
13#[cfg(feature = "metrics")]
14use crate::metrics::{self, MetricAggregator};
15use crate::protocol::{ClientSdkInfo, Event};
16use crate::session::SessionFlusher;
17use crate::types::{Dsn, Uuid};
18use crate::{ClientOptions, Envelope, Hub, Integration, Scope, SessionMode, Transport};
19
20impl<T: Into<ClientOptions>> From<T> for Client {
21    fn from(o: T) -> Client {
22        Client::with_options(o.into())
23    }
24}
25
26pub(crate) type TransportArc = Arc<RwLock<Option<Arc<dyn Transport>>>>;
27
28/// The Sentry Client.
29///
30/// The Client is responsible for event processing and sending events to the
31/// sentry server via the configured [`Transport`]. It can be created from a
32/// [`ClientOptions`].
33///
34/// See the [Unified API] document for more details.
35///
36/// # Examples
37///
38/// ```
39/// sentry::Client::from(sentry::ClientOptions::default());
40/// ```
41///
42/// [`ClientOptions`]: struct.ClientOptions.html
43/// [`Transport`]: trait.Transport.html
44/// [Unified API]: https://develop.sentry.dev/sdk/unified-api/
45pub struct Client {
46    options: ClientOptions,
47    transport: TransportArc,
48    session_flusher: RwLock<Option<SessionFlusher>>,
49    #[cfg(feature = "metrics")]
50    metric_aggregator: RwLock<Option<MetricAggregator>>,
51    integrations: Vec<(TypeId, Arc<dyn Integration>)>,
52    pub(crate) sdk_info: ClientSdkInfo,
53}
54
55impl fmt::Debug for Client {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.debug_struct("Client")
58            .field("dsn", &self.dsn())
59            .field("options", &self.options)
60            .finish()
61    }
62}
63
64impl Clone for Client {
65    fn clone(&self) -> Client {
66        let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone()));
67        let session_flusher = RwLock::new(Some(SessionFlusher::new(
68            transport.clone(),
69            self.options.session_mode,
70        )));
71        #[cfg(feature = "metrics")]
72        let metric_aggregator = RwLock::new(Some(MetricAggregator::new(
73            transport.clone(),
74            &self.options,
75        )));
76        Client {
77            options: self.options.clone(),
78            transport,
79            session_flusher,
80            #[cfg(feature = "metrics")]
81            metric_aggregator,
82            integrations: self.integrations.clone(),
83            sdk_info: self.sdk_info.clone(),
84        }
85    }
86}
87
88impl Client {
89    /// Creates a new Sentry client from a config.
90    ///
91    /// # Supported Configs
92    ///
93    /// The following common values are supported for the client config:
94    ///
95    /// * `ClientOptions`: configure the client with the given client options.
96    /// * `()` or empty string: Disable the client.
97    /// * `&str` / `String` / `&OsStr` / `String`: configure the client with the given DSN.
98    /// * `Dsn` / `&Dsn`: configure the client with a given DSN.
99    /// * `(Dsn, ClientOptions)`: configure the client from the given DSN and optional options.
100    ///
101    /// The `Default` implementation of `ClientOptions` pulls in the DSN from the
102    /// `SENTRY_DSN` environment variable.
103    ///
104    /// # Panics
105    ///
106    /// The `Into<ClientOptions>` implementations can panic for the forms where a DSN needs to be
107    /// parsed.  If you want to handle invalid DSNs you need to parse them manually by calling
108    /// parse on it and handle the error.
109    pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
110        Client::with_options(opts.into())
111    }
112
113    /// Creates a new sentry client for the given options.
114    ///
115    /// If the DSN on the options is set to `None` the client will be entirely
116    /// disabled.
117    pub fn with_options(mut options: ClientOptions) -> Client {
118        // Create the main hub eagerly to avoid problems with the background thread
119        // See https://github.com/getsentry/sentry-rust/issues/237
120        Hub::with(|_| {});
121
122        let create_transport = || {
123            options.dsn.as_ref()?;
124            let factory = options.transport.as_ref()?;
125            Some(factory.create_transport(&options))
126        };
127
128        let transport = Arc::new(RwLock::new(create_transport()));
129
130        let mut sdk_info = SDK_INFO.clone();
131
132        // NOTE: We do not filter out duplicate integrations based on their
133        // TypeId.
134        let integrations: Vec<_> = options
135            .integrations
136            .iter()
137            .map(|integration| (integration.as_ref().type_id(), integration.clone()))
138            .collect();
139
140        for (_, integration) in integrations.iter() {
141            integration.setup(&mut options);
142            sdk_info.integrations.push(integration.name().to_string());
143        }
144
145        let session_flusher = RwLock::new(Some(SessionFlusher::new(
146            transport.clone(),
147            options.session_mode,
148        )));
149
150        #[cfg(feature = "metrics")]
151        let metric_aggregator =
152            RwLock::new(Some(MetricAggregator::new(transport.clone(), &options)));
153
154        Client {
155            options,
156            transport,
157            session_flusher,
158            #[cfg(feature = "metrics")]
159            metric_aggregator,
160            integrations,
161            sdk_info,
162        }
163    }
164
165    pub(crate) fn get_integration<I>(&self) -> Option<&I>
166    where
167        I: Integration,
168    {
169        let id = TypeId::of::<I>();
170        let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
171        integration.as_ref().as_any().downcast_ref()
172    }
173
174    /// Prepares an event for transmission to sentry.
175    pub fn prepare_event(
176        &self,
177        mut event: Event<'static>,
178        scope: Option<&Scope>,
179    ) -> Option<Event<'static>> {
180        // event_id and sdk_info are set before the processors run so that the
181        // processors can poke around in that data.
182        if event.event_id.is_nil() {
183            event.event_id = random_uuid();
184        }
185
186        if event.sdk.is_none() {
187            // NOTE: we need to clone here because `Event` must be `'static`
188            event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
189        }
190
191        if let Some(scope) = scope {
192            event = match scope.apply_to_event(event) {
193                Some(event) => event,
194                None => return None,
195            };
196        }
197
198        for (_, integration) in self.integrations.iter() {
199            let id = event.event_id;
200            event = match integration.process_event(event, &self.options) {
201                Some(event) => event,
202                None => {
203                    sentry_debug!("integration dropped event {:?}", id);
204                    return None;
205                }
206            }
207        }
208
209        if event.release.is_none() {
210            event.release.clone_from(&self.options.release);
211        }
212        if event.environment.is_none() {
213            event.environment.clone_from(&self.options.environment);
214        }
215        if event.server_name.is_none() {
216            event.server_name.clone_from(&self.options.server_name);
217        }
218        if &event.platform == "other" {
219            event.platform = "native".into();
220        }
221
222        if let Some(ref func) = self.options.before_send {
223            sentry_debug!("invoking before_send callback");
224            let id = event.event_id;
225            if let Some(processed_event) = func(event) {
226                event = processed_event;
227            } else {
228                sentry_debug!("before_send dropped event {:?}", id);
229                return None;
230            }
231        }
232
233        if let Some(scope) = scope {
234            scope.update_session_from_event(&event);
235        }
236
237        if !self.sample_should_send(self.options.sample_rate) {
238            None
239        } else {
240            Some(event)
241        }
242    }
243
244    /// Returns the options of this client.
245    pub fn options(&self) -> &ClientOptions {
246        &self.options
247    }
248
249    /// Returns the DSN that constructed this client.
250    pub fn dsn(&self) -> Option<&Dsn> {
251        self.options.dsn.as_ref()
252    }
253
254    /// Quick check to see if the client is enabled.
255    ///
256    /// The Client is enabled if it has a valid DSN and Transport configured.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use std::sync::Arc;
262    ///
263    /// let client = sentry::Client::from(sentry::ClientOptions::default());
264    /// assert!(!client.is_enabled());
265    ///
266    /// let dsn = "https://public@example.com/1";
267    /// let transport = sentry::test::TestTransport::new();
268    /// let client = sentry::Client::from((
269    ///     dsn,
270    ///     sentry::ClientOptions {
271    ///         transport: Some(Arc::new(transport)),
272    ///         ..Default::default()
273    ///     },
274    /// ));
275    /// assert!(client.is_enabled());
276    /// ```
277    pub fn is_enabled(&self) -> bool {
278        self.options.dsn.is_some() && self.transport.read().unwrap().is_some()
279    }
280
281    /// Captures an event and sends it to sentry.
282    pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
283        if let Some(ref transport) = *self.transport.read().unwrap() {
284            if let Some(event) = self.prepare_event(event, scope) {
285                let event_id = event.event_id;
286                let mut envelope: Envelope = event.into();
287                // For request-mode sessions, we aggregate them all instead of
288                // flushing them out early.
289                if self.options.session_mode == SessionMode::Application {
290                    let session_item = scope.and_then(|scope| {
291                        scope
292                            .session
293                            .lock()
294                            .unwrap()
295                            .as_mut()
296                            .and_then(|session| session.create_envelope_item())
297                    });
298                    if let Some(session_item) = session_item {
299                        envelope.add_item(session_item);
300                    }
301                }
302
303                if let Some(scope) = scope {
304                    for attachment in scope.attachments.iter().cloned() {
305                        envelope.add_item(attachment);
306                    }
307                }
308
309                transport.send_envelope(envelope);
310                return event_id;
311            }
312        }
313        Default::default()
314    }
315
316    /// Sends the specified [`Envelope`] to sentry.
317    pub fn send_envelope(&self, envelope: Envelope) {
318        if let Some(ref transport) = *self.transport.read().unwrap() {
319            transport.send_envelope(envelope);
320        }
321    }
322
323    pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
324        if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
325            flusher.enqueue(session_update);
326        }
327    }
328
329    /// Captures a metric and sends it to Sentry on the next flush.
330    #[cfg(feature = "metrics")]
331    pub fn add_metric(&self, metric: metrics::Metric) {
332        if let Some(ref aggregator) = *self.metric_aggregator.read().unwrap() {
333            aggregator.add(metric)
334        }
335    }
336
337    /// Drains all pending events without shutting down.
338    pub fn flush(&self, timeout: Option<Duration>) -> bool {
339        if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
340            flusher.flush();
341        }
342        #[cfg(feature = "metrics")]
343        if let Some(ref aggregator) = *self.metric_aggregator.read().unwrap() {
344            aggregator.flush();
345        }
346        if let Some(ref transport) = *self.transport.read().unwrap() {
347            transport.flush(timeout.unwrap_or(self.options.shutdown_timeout))
348        } else {
349            true
350        }
351    }
352
353    /// Drains all pending events and shuts down the transport behind the
354    /// client.  After shutting down the transport is removed.
355    ///
356    /// This returns `true` if the queue was successfully drained in the
357    /// given time or `false` if not (for instance because of a timeout).
358    /// If no timeout is provided the client will wait for as long a
359    /// `shutdown_timeout` in the client options.
360    pub fn close(&self, timeout: Option<Duration>) -> bool {
361        drop(self.session_flusher.write().unwrap().take());
362        #[cfg(feature = "metrics")]
363        drop(self.metric_aggregator.write().unwrap().take());
364        let transport_opt = self.transport.write().unwrap().take();
365        if let Some(transport) = transport_opt {
366            sentry_debug!("client close; request transport to shut down");
367            transport.shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
368        } else {
369            sentry_debug!("client close; no transport to shut down");
370            true
371        }
372    }
373
374    /// Returns a random boolean with a probability defined
375    /// by rate
376    pub fn sample_should_send(&self, rate: f32) -> bool {
377        if rate >= 1.0 {
378            true
379        } else if rate <= 0.0 {
380            false
381        } else {
382            random::<f32>() < rate
383        }
384    }
385}
386
387// Make this unwind safe. It's not out of the box because of the
388// `BeforeCallback`s inside `ClientOptions`, and the contained Integrations
389impl RefUnwindSafe for Client {}