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