Skip to main content

sentry_core/
hub.rs

1use std::sync::{Arc, RwLock};
2
3#[cfg(feature = "metrics")]
4use crate::metrics::IntoProtocolMetric;
5#[cfg(feature = "logs")]
6use crate::protocol::Log;
7#[cfg(feature = "release-health")]
8use crate::protocol::SessionStatus;
9use crate::protocol::{Event, Level};
10use crate::types::Uuid;
11use crate::{Integration, IntoBreadcrumbs, Scope, ScopeGuard};
12
13/// Marks values as used in minimal builds where `with_client_impl!` turns many
14/// method bodies into no-ops and would otherwise leave their parameters unused.
15macro_rules! use_without_client {
16    ($($value:expr),+ $(,)?) => {
17        #[cfg(not(feature = "client"))]
18        let _ = ($( &$value ),+,);
19    };
20}
21
22/// The central object that can manage scopes and clients.
23///
24/// This can be used to capture events and manage the scope. Although `Hub` is [`Send`] and
25/// [`Sync`], sharing a hub between concurrent threads or tasks can lead to unexpected behavior,
26/// including panics. Prefer using a separate hub for each concurrent thread or task.
27///
28/// Each thread has its own thread-local ( see [`Hub::current`]) hub, which is
29/// automatically derived from the main hub ([`Hub::main`]).
30///
31/// In most situations, developers do not need to interface with the hub directly.  Instead
32/// toplevel convenience functions are exposed that will automatically dispatch
33/// to the thread-local ([`Hub::current`]) hub.  In some situations, this might not be
34/// possible, in which case it might become necessary to manually work with the
35/// hub.  See the main [`crate`] docs for some common use-cases and pitfalls
36/// related to parallel, concurrent or async code.
37///
38/// Hubs that are wrapped in [`Arc`]s can be bound to the current thread with
39/// the `run` static method.
40///
41/// Most common operations:
42///
43/// * [`Hub::new`]: creates a brand new hub
44/// * [`Hub::current`]: returns the thread local hub
45/// * [`Hub::with`]: invoke a callback with the thread local hub
46/// * [`Hub::with_active`]: like `Hub::with` but does not invoke the callback if
47///   the client is not in a supported state or not bound
48/// * [`Hub::new_from_top`]: creates a new hub with just the top scope of another hub.
49#[derive(Debug)]
50pub struct Hub {
51    #[cfg(feature = "client")]
52    pub(crate) inner: crate::hub_impl::HubImpl,
53    pub(crate) last_event_id: RwLock<Option<Uuid>>,
54}
55
56impl Hub {
57    /// Like [`Hub::with`] but only calls the function if a client is bound.
58    ///
59    /// This is useful for integrations that want to do efficiently nothing if there is no
60    /// client bound.  Additionally this internally ensures that the client can be safely
61    /// synchronized.  This prevents accidental recursive calls into the client.
62    pub fn with_active<F, R>(f: F) -> R
63    where
64        F: FnOnce(&Arc<Hub>) -> R,
65        R: Default,
66    {
67        use_without_client!(f);
68        with_client_impl! {{
69            let hub = Hub::current();
70            if hub.is_active_and_usage_safe() {
71                f(&hub)
72            } else {
73                Default::default()
74            }
75
76        }}
77    }
78
79    /// Looks up an integration on the hub.
80    ///
81    /// Calls the given function with the requested integration instance when it
82    /// is active on the currently active client.
83    ///
84    /// See the global [`capture_event`](fn.capture_event.html)
85    /// for more documentation.
86    pub fn with_integration<I, F, R>(&self, f: F) -> R
87    where
88        I: Integration,
89        F: FnOnce(&I) -> R,
90        R: Default,
91    {
92        use_without_client!(f);
93        with_client_impl! {{
94            if let Some(client) = self.client() {
95                if let Some(integration) = client.get_integration::<I>() {
96                    return f(integration);
97                }
98            }
99            Default::default()
100        }}
101    }
102
103    /// Returns the last event id.
104    pub fn last_event_id(&self) -> Option<Uuid> {
105        *self.last_event_id.read().unwrap()
106    }
107
108    /// Sends the event to the current client with the current scope.
109    ///
110    /// In case no client is bound this does nothing instead.
111    ///
112    /// See the global [`capture_event`](fn.capture_event.html)
113    /// for more documentation.
114    pub fn capture_event(&self, event: Event<'static>) -> Uuid {
115        use_without_client!(event);
116        with_client_impl! {{
117            let top = self.inner.with(|stack| stack.top().clone());
118            let Some(ref client) = top.client else { return Default::default() };
119            let event_id = client.capture_event(event, Some(&top.scope));
120            *self.last_event_id.write().unwrap() = Some(event_id);
121            event_id
122        }}
123    }
124
125    /// Captures an arbitrary message.
126    ///
127    /// See the global [`capture_message`](fn.capture_message.html)
128    /// for more documentation.
129    pub fn capture_message(&self, msg: &str, level: Level) -> Uuid {
130        use_without_client!(msg, level);
131        with_client_impl! {{
132            let event = Event {
133                message: Some(msg.to_string()),
134                level,
135                ..Default::default()
136            };
137            self.capture_event(event)
138        }}
139    }
140
141    /// Start a new session for Release Health.
142    ///
143    /// See the global [`start_session`](fn.start_session.html)
144    /// for more documentation.
145    #[cfg(feature = "release-health")]
146    pub fn start_session(&self) {
147        with_client_impl! {{
148            self.inner.with_mut(|stack| {
149                let top = stack.top_mut();
150                if let Some(session) = crate::session::Session::from_stack(top) {
151                    // When creating a *new* session, we make sure it is unique,
152                    // as to no inherit *backwards* to any parents.
153                    let scope = Arc::make_mut(&mut top.scope);
154                    scope.session = Arc::new(std::sync::Mutex::new(Some(session)));
155                }
156            })
157        }}
158    }
159
160    /// End the current Release Health Session.
161    ///
162    /// See the global [`sentry::end_session`](crate::end_session) for more documentation.
163    #[cfg(feature = "release-health")]
164    pub fn end_session(&self) {
165        self.end_session_with_status(SessionStatus::Exited)
166    }
167
168    /// End the current Release Health Session with the given [`SessionStatus`].
169    ///
170    /// See the global [`end_session_with_status`](crate::end_session_with_status)
171    /// for more documentation.
172    #[cfg(feature = "release-health")]
173    pub fn end_session_with_status(&self, status: SessionStatus) {
174        use_without_client!(status);
175        with_client_impl! {{
176            self.inner.with_mut(|stack| {
177                let top = stack.top_mut();
178                // drop will close and enqueue the session
179                if let Some(mut session) = top.scope.session.lock().unwrap().take() {
180                    session.close(status);
181                }
182            })
183        }}
184    }
185
186    /// Pushes a new scope.
187    ///
188    /// This returns a guard that when dropped will pop the scope again.
189    pub fn push_scope(&self) -> ScopeGuard {
190        with_client_impl! {{
191            self.inner.with_mut(|stack| {
192                stack.push();
193                ScopeGuard(Some((self.inner.stack.clone(), stack.depth())))
194            })
195        }}
196    }
197
198    /// Temporarily pushes a scope for a single call optionally reconfiguring it.
199    ///
200    /// See the global [`with_scope`](fn.with_scope.html)
201    /// for more documentation.
202    pub fn with_scope<C, F, R>(&self, scope_config: C, callback: F) -> R
203    where
204        C: FnOnce(&mut Scope),
205        F: FnOnce() -> R,
206    {
207        use_without_client!(scope_config);
208        #[cfg(feature = "client")]
209        {
210            let _guard = self.push_scope();
211            self.configure_scope(scope_config);
212            callback()
213        }
214        #[cfg(not(feature = "client"))]
215        {
216            callback()
217        }
218    }
219
220    /// Invokes a function that can modify the current scope.
221    ///
222    /// This method should not be called concurrently on the same hub, as updates are not atomic
223    /// and may be lost.
224    ///
225    /// See the global [`configure_scope`](fn.configure_scope.html)
226    /// for more documentation.
227    pub fn configure_scope<F, R>(&self, f: F) -> R
228    where
229        R: Default,
230        F: FnOnce(&mut Scope) -> R,
231    {
232        use_without_client!(f);
233        with_client_impl! {{
234            let mut new_scope = self.with_current_scope(|scope| scope.clone());
235            let rv = f(&mut new_scope);
236            self.with_current_scope_mut(|ptr| *ptr = new_scope);
237            rv
238        }}
239    }
240
241    /// Adds a new breadcrumb to the current scope.
242    ///
243    /// See the global [`add_breadcrumb`](fn.add_breadcrumb.html)
244    /// for more documentation.
245    pub fn add_breadcrumb<B: IntoBreadcrumbs>(&self, breadcrumb: B) {
246        use_without_client!(breadcrumb);
247        with_client_impl! {{
248            self.inner.with_mut(|stack| {
249                let top = stack.top_mut();
250                if let Some(ref client) = top.client {
251                    let scope = Arc::make_mut(&mut top.scope);
252                    let options = client.options();
253                    let breadcrumbs = Arc::make_mut(&mut scope.breadcrumbs);
254                    for breadcrumb in breadcrumb.into_breadcrumbs() {
255                        let breadcrumb_opt = match options.before_breadcrumb {
256                            Some(ref callback) => callback(breadcrumb),
257                            None => Some(breadcrumb)
258                        };
259                        if let Some(breadcrumb) = breadcrumb_opt {
260                            breadcrumbs.push_back(breadcrumb);
261                        }
262                        while breadcrumbs.len() > options.max_breadcrumbs {
263                            breadcrumbs.pop_front();
264                        }
265                    }
266                }
267            })
268        }}
269    }
270
271    /// Captures a structured log.
272    #[cfg(feature = "logs")]
273    pub fn capture_log(&self, log: Log) {
274        use_without_client!(log);
275        with_client_impl! {{
276            let top = self.inner.with(|stack| stack.top().clone());
277            let Some(ref client) = top.client else { return };
278            client.capture_log(log, &top.scope);
279        }}
280    }
281
282    /// Captures a metric on this hub, sending it to Sentry.
283    ///
284    /// If this hub has no client, the metric is dropped.
285    #[cfg(feature = "metrics")]
286    pub fn capture_metric<M: IntoProtocolMetric>(&self, metric: M) {
287        use_without_client!(metric);
288        with_client_impl! {{
289            let top = self.inner.with(|stack| stack.top().clone());
290            let Some(ref client) = top.client else { return };
291            client.capture_metric(metric, &top.scope);
292        }}
293    }
294}