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