From ff1452893843a9c858072ec6956901d1ee45659d Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Tue, 11 Mar 2025 14:11:53 +0100 Subject: [PATCH 01/13] feat: introduce `release-health `feature --- sentry-actix/Cargo.toml | 1 + sentry-core/Cargo.toml | 3 ++- sentry-core/src/api.rs | 3 +++ sentry-core/src/hub.rs | 3 +++ sentry/Cargo.toml | 10 +++++++++- sentry/src/init.rs | 2 ++ 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/sentry-actix/Cargo.toml b/sentry-actix/Cargo.toml index d9554bb04..12a77d944 100644 --- a/sentry-actix/Cargo.toml +++ b/sentry-actix/Cargo.toml @@ -17,6 +17,7 @@ actix-web = { version = "4", default-features = false } futures-util = { version = "0.3.5", default-features = false } sentry-core = { version = "0.36.0", path = "../sentry-core", default-features = false, features = [ "client", + "release-health", ] } actix-http = "3.9.0" diff --git a/sentry-core/Cargo.toml b/sentry-core/Cargo.toml index e372cb1eb..b00c26d32 100644 --- a/sentry-core/Cargo.toml +++ b/sentry-core/Cargo.toml @@ -25,7 +25,8 @@ client = ["rand"] # I would love to just have a `log` feature, but this is used inside a macro, # and macros actually expand features (and extern crate) where they are used! debug-logs = ["dep:log"] -test = ["client"] +test = ["client", "release-health"] +release-health = [] [dependencies] cadence = { version = "1.4.0", optional = true } diff --git a/sentry-core/src/api.rs b/sentry-core/src/api.rs index 06164e4e3..47cc0671b 100644 --- a/sentry-core/src/api.rs +++ b/sentry-core/src/api.rs @@ -279,11 +279,13 @@ pub fn last_event_id() -> Option { /// /// sentry::end_session(); /// ``` +#[cfg(feature = "release-health")] pub fn start_session() { Hub::with_active(|hub| hub.start_session()) } /// End the current Release Health Session. +#[cfg(feature = "release-health")] pub fn end_session() { end_session_with_status(SessionStatus::Exited) } @@ -295,6 +297,7 @@ pub fn end_session() { /// /// When an `Abnormal` session should be captured, it has to be done explicitly /// using this function. +#[cfg(feature = "release-health")] pub fn end_session_with_status(status: SessionStatus) { Hub::with_active(|hub| hub.end_session_with_status(status)) } diff --git a/sentry-core/src/hub.rs b/sentry-core/src/hub.rs index f5ebf155c..6bab5263d 100644 --- a/sentry-core/src/hub.rs +++ b/sentry-core/src/hub.rs @@ -126,6 +126,7 @@ impl Hub { /// /// See the global [`start_session`](fn.start_session.html) /// for more documentation. + #[cfg(feature = "release-health")] pub fn start_session(&self) { with_client_impl! {{ self.inner.with_mut(|stack| { @@ -143,6 +144,7 @@ impl Hub { /// End the current Release Health Session. /// /// See the global [`sentry::end_session`](crate::end_session) for more documentation. + #[cfg(feature = "release-health")] pub fn end_session(&self) { self.end_session_with_status(SessionStatus::Exited) } @@ -151,6 +153,7 @@ impl Hub { /// /// See the global [`end_session_with_status`](crate::end_session_with_status) /// for more documentation. + #[cfg(feature = "release-health")] pub fn end_session_with_status(&self, status: SessionStatus) { with_client_impl! {{ self.inner.with_mut(|stack| { diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index 50ece0ca8..c8bfe1c90 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -21,7 +21,14 @@ all-features = true rustdoc-args = ["--cfg", "doc_cfg"] [features] -default = ["backtrace", "contexts", "debug-images", "panic", "transport"] +default = [ + "backtrace", + "contexts", + "debug-images", + "panic", + "transport", + "release-health", +] # default integrations backtrace = ["sentry-backtrace", "sentry-tracing?/backtrace"] @@ -39,6 +46,7 @@ tracing = ["sentry-tracing"] # other features test = ["sentry-core/test"] debug-logs = ["dep:log", "sentry-core/debug-logs"] +release-health = ["sentry-core/release-health"] # transports transport = ["reqwest", "native-tls"] reqwest = ["dep:reqwest", "httpdate", "tokio"] diff --git a/sentry/src/init.rs b/sentry/src/init.rs index befb66fd6..04481a119 100644 --- a/sentry/src/init.rs +++ b/sentry/src/init.rs @@ -34,6 +34,7 @@ impl Drop for ClientInitGuard { sentry_debug!("dropping client guard (no client to dispose)"); } // end any session that might be open before closing the client + #[cfg(feature = "release-health")] crate::end_session(); self.0.close(None); } @@ -103,6 +104,7 @@ where } else { sentry_debug!("initialized disabled sentry client due to disabled or invalid DSN"); } + #[cfg(feature = "release-health")] if auto_session_tracking && session_mode == SessionMode::Application { crate::start_session() } From c18a9c87954a1a0a506de23b6dba2eb6223fb498 Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Tue, 11 Mar 2025 14:23:34 +0100 Subject: [PATCH 02/13] fix: gate `session_flusher` --- sentry-core/src/client.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index f95154dea..92d49eaa2 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -60,10 +60,14 @@ impl fmt::Debug for Client { impl Clone for Client { fn clone(&self) -> Client { let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone())); - let session_flusher = RwLock::new(Some(SessionFlusher::new( - transport.clone(), - self.options.session_mode, - ))); + let session_flusher = if cfg!(feature = "release-health") { + RwLock::new(Some(SessionFlusher::new( + transport.clone(), + self.options.session_mode, + ))) + } else { + RwLock::new(None) + }; Client { options: self.options.clone(), transport, From a6dd24d9afe5cd99dac01e27cdc9887ee0966dfd Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Tue, 11 Mar 2025 14:42:56 +0100 Subject: [PATCH 03/13] fix: forgot usage of `SessionFlusher` --- sentry-core/src/client.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index 92d49eaa2..a1b7fdfad 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -135,10 +135,14 @@ impl Client { sdk_info.integrations.push(integration.name().to_string()); } - let session_flusher = RwLock::new(Some(SessionFlusher::new( - transport.clone(), - options.session_mode, - ))); + let session_flusher = if cfg!(feature = "release-health") { + RwLock::new(Some(SessionFlusher::new( + transport.clone(), + options.session_mode, + ))) + } else { + RwLock::new(None) + }; Client { options, From 627115f5279cc556afeb79bcda8969f1038d776e Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Wed, 12 Mar 2025 14:39:07 +0100 Subject: [PATCH 04/13] feat: gate `auto_session_tracking` and `session_mode` behind feature --- sentry-core/src/client.rs | 1 + sentry-core/src/clientoptions.rs | 16 +++++++++++++--- sentry-core/src/session.rs | 1 + sentry/src/init.rs | 4 ++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index a1b7fdfad..b5134276e 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -274,6 +274,7 @@ impl Client { let mut envelope: Envelope = event.into(); // For request-mode sessions, we aggregate them all instead of // flushing them out early. + #[cfg(feature = "release-health")] if self.options.session_mode == SessionMode::Application { let session_item = scope.and_then(|scope| { scope diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 583aae8cc..53c21f336 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -171,8 +171,10 @@ pub struct ClientOptions { /// When automatic session tracking is enabled, a new "user-mode" session /// is started at the time of `sentry::init`, and will persist for the /// application lifetime. + #[cfg(feature = "release-health")] pub auto_session_tracking: bool, /// Determine how Sessions are being tracked. + #[cfg(feature = "release-health")] pub session_mode: SessionMode, /// Border frames which indicate a border from a backtrace to /// useless internals. Some are automatically included. @@ -223,7 +225,8 @@ impl fmt::Debug for ClientOptions { let integrations: Vec<_> = self.integrations.iter().map(|i| i.name()).collect(); - f.debug_struct("ClientOptions") + let mut debug_struct = f.debug_struct("ClientOptions"); + debug_struct .field("dsn", &self.dsn) .field("debug", &self.debug) .field("release", &self.release) @@ -251,9 +254,14 @@ impl fmt::Debug for ClientOptions { .field("http_proxy", &self.http_proxy) .field("https_proxy", &self.https_proxy) .field("shutdown_timeout", &self.shutdown_timeout) - .field("accept_invalid_certs", &self.accept_invalid_certs) + .field("accept_invalid_certs", &self.accept_invalid_certs); + + #[cfg(feature = "release-health")] + debug_struct .field("auto_session_tracking", &self.auto_session_tracking) - .field("session_mode", &self.session_mode) + .field("session_mode", &self.session_mode); + + debug_struct .field("extra_border_frames", &self.extra_border_frames) .field("trim_backtraces", &self.trim_backtraces) .field("user_agent", &self.user_agent) @@ -286,7 +294,9 @@ impl Default for ClientOptions { https_proxy: None, shutdown_timeout: Duration::from_secs(2), accept_invalid_certs: false, + #[cfg(feature = "release-health")] auto_session_tracking: false, + #[cfg(feature = "release-health")] session_mode: SessionMode::Application, extra_border_frames: vec![], trim_backtraces: true, diff --git a/sentry-core/src/session.rs b/sentry-core/src/session.rs index 1b26af74e..dc435f9a0 100644 --- a/sentry-core/src/session.rs +++ b/sentry-core/src/session.rs @@ -447,6 +447,7 @@ mod tests { }, crate::ClientOptions { release: Some("some-release".into()), + #[cfg(feature = "release-health")] session_mode: SessionMode::Request, ..Default::default() }, diff --git a/sentry/src/init.rs b/sentry/src/init.rs index 04481a119..a98ee1beb 100644 --- a/sentry/src/init.rs +++ b/sentry/src/init.rs @@ -94,8 +94,12 @@ where C: Into, { let opts = apply_defaults(opts.into()); + + #[cfg(feature = "release-health")] let auto_session_tracking = opts.auto_session_tracking; + #[cfg(feature = "release-health")] let session_mode = opts.session_mode; + let client = Arc::new(Client::from(opts)); Hub::with(|hub| hub.bind_client(Some(client.clone()))); From 32bc70923be21bc53dd2cb586e34389261de0ec4 Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Wed, 12 Mar 2025 14:45:16 +0100 Subject: [PATCH 05/13] fix: use cfg attribute instead --- sentry-core/src/client.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index b5134276e..f565dcff1 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -60,14 +60,15 @@ impl fmt::Debug for Client { impl Clone for Client { fn clone(&self) -> Client { let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone())); - let session_flusher = if cfg!(feature = "release-health") { - RwLock::new(Some(SessionFlusher::new( - transport.clone(), - self.options.session_mode, - ))) - } else { - RwLock::new(None) - }; + + #[cfg(feature = "release-health")] + let session_flusher = RwLock::new(Some(SessionFlusher::new( + transport.clone(), + self.options.session_mode, + ))); + #[cfg(not(feature = "release-health"))] + let session_flusher = RwLock::new(None); + Client { options: self.options.clone(), transport, @@ -135,14 +136,13 @@ impl Client { sdk_info.integrations.push(integration.name().to_string()); } - let session_flusher = if cfg!(feature = "release-health") { - RwLock::new(Some(SessionFlusher::new( - transport.clone(), - options.session_mode, - ))) - } else { - RwLock::new(None) - }; + #[cfg(feature = "release-health")] + let session_flusher = RwLock::new(Some(SessionFlusher::new( + transport.clone(), + options.session_mode, + ))); + #[cfg(not(feature = "release-health"))] + let session_flusher = RwLock::new(None); Client { options, From eb247f74b1bee4ea756d369cb2394f2560d59d49 Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Tue, 18 Mar 2025 15:25:29 +0100 Subject: [PATCH 06/13] fix: don't import `SessionMode` on non-release health features --- sentry/src/init.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry/src/init.rs b/sentry/src/init.rs index a98ee1beb..12527f0d3 100644 --- a/sentry/src/init.rs +++ b/sentry/src/init.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use sentry_core::{sentry_debug, SessionMode}; +use sentry_core::sentry_debug; +#[cfg(feature = "release-health")] +use sentry_core::SessionMode; use crate::defaults::apply_defaults; use crate::{Client, ClientOptions, Hub}; From 6ecea9264e1fe790d1e4233045ec4a82938067a1 Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Tue, 18 Mar 2025 15:32:36 +0100 Subject: [PATCH 07/13] fix: forgot unused --- sentry-core/src/api.rs | 1 + sentry-core/src/client.rs | 4 +++- sentry-core/src/session.rs | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sentry-core/src/api.rs b/sentry-core/src/api.rs index 47cc0671b..ea0b8532f 100644 --- a/sentry-core/src/api.rs +++ b/sentry-core/src/api.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "release-health")] use sentry_types::protocol::v7::SessionStatus; use crate::protocol::{Event, Level}; diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index f565dcff1..5f7383237 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -13,7 +13,9 @@ use crate::constants::SDK_INFO; use crate::protocol::{ClientSdkInfo, Event}; use crate::session::SessionFlusher; use crate::types::{Dsn, Uuid}; -use crate::{ClientOptions, Envelope, Hub, Integration, Scope, SessionMode, Transport}; +#[cfg(feature = "release-health")] +use crate::SessionMode; +use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport}; impl> From for Client { fn from(o: T) -> Client { diff --git a/sentry-core/src/session.rs b/sentry-core/src/session.rs index dc435f9a0..c7c8d1bcb 100644 --- a/sentry-core/src/session.rs +++ b/sentry-core/src/session.rs @@ -13,7 +13,11 @@ use crate::protocol::{ EnvelopeItem, Event, Level, SessionAggregateItem, SessionAggregates, SessionAttributes, SessionStatus, SessionUpdate, }; + +#[cfg(feature = "release-health")] use crate::scope::StackLayer; + +#[cfg(feature = "release-health")] use crate::types::random_uuid; use crate::{Client, Envelope}; @@ -35,6 +39,7 @@ impl Drop for Session { } impl Session { + #[cfg(feature = "release-health")] pub fn from_stack(stack: &StackLayer) -> Option { let client = stack.client.as_ref()?; let options = client.options(); @@ -110,6 +115,7 @@ impl Session { } } + #[cfg(feature = "release-health")] pub(crate) fn create_envelope_item(&mut self) -> Option { if self.dirty { let item = self.session_update.clone().into(); @@ -123,6 +129,7 @@ impl Session { // as defined here: https://develop.sentry.dev/sdk/envelopes/#size-limits const MAX_SESSION_ITEMS: usize = 100; +#[cfg(feature = "release-health")] const FLUSH_INTERVAL: Duration = Duration::from_secs(60); #[derive(Debug, Default)] @@ -189,6 +196,7 @@ pub(crate) struct SessionFlusher { impl SessionFlusher { /// Creates a new Flusher that will submit envelopes to the given `transport`. + #[cfg(feature = "release-health")] pub fn new(transport: TransportArc, mode: SessionMode) -> Self { let queue = Arc::new(Mutex::new(Default::default())); #[allow(clippy::mutex_atomic)] From 1f4c220af8abe5a86197706d700437a58e5b647e Mon Sep 17 00:00:00 2001 From: Jack Lavigne Date: Wed, 19 Mar 2025 21:27:45 +0100 Subject: [PATCH 08/13] fix: don't use feature gate in struct fields --- sentry-core/src/clientoptions.rs | 14 +++----------- sentry/src/init.rs | 2 -- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 53c21f336..38ec61a7a 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -171,10 +171,8 @@ pub struct ClientOptions { /// When automatic session tracking is enabled, a new "user-mode" session /// is started at the time of `sentry::init`, and will persist for the /// application lifetime. - #[cfg(feature = "release-health")] pub auto_session_tracking: bool, /// Determine how Sessions are being tracked. - #[cfg(feature = "release-health")] pub session_mode: SessionMode, /// Border frames which indicate a border from a backtrace to /// useless internals. Some are automatically included. @@ -225,8 +223,7 @@ impl fmt::Debug for ClientOptions { let integrations: Vec<_> = self.integrations.iter().map(|i| i.name()).collect(); - let mut debug_struct = f.debug_struct("ClientOptions"); - debug_struct + f.debug_struct("ClientOptions") .field("dsn", &self.dsn) .field("debug", &self.debug) .field("release", &self.release) @@ -254,14 +251,9 @@ impl fmt::Debug for ClientOptions { .field("http_proxy", &self.http_proxy) .field("https_proxy", &self.https_proxy) .field("shutdown_timeout", &self.shutdown_timeout) - .field("accept_invalid_certs", &self.accept_invalid_certs); - - #[cfg(feature = "release-health")] - debug_struct + .field("accept_invalid_certs", &self.accept_invalid_certs) .field("auto_session_tracking", &self.auto_session_tracking) - .field("session_mode", &self.session_mode); - - debug_struct + .field("session_mode", &self.session_mode) .field("extra_border_frames", &self.extra_border_frames) .field("trim_backtraces", &self.trim_backtraces) .field("user_agent", &self.user_agent) diff --git a/sentry/src/init.rs b/sentry/src/init.rs index 12527f0d3..eeee2e3d4 100644 --- a/sentry/src/init.rs +++ b/sentry/src/init.rs @@ -97,9 +97,7 @@ where { let opts = apply_defaults(opts.into()); - #[cfg(feature = "release-health")] let auto_session_tracking = opts.auto_session_tracking; - #[cfg(feature = "release-health")] let session_mode = opts.session_mode; let client = Arc::new(Client::from(opts)); From 92878b85502ae477e234bce442bb721262a438da Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 20 Mar 2025 11:26:57 +0100 Subject: [PATCH 09/13] fix --- sentry-core/src/clientoptions.rs | 2 -- sentry/src/init.rs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 38ec61a7a..583aae8cc 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -286,9 +286,7 @@ impl Default for ClientOptions { https_proxy: None, shutdown_timeout: Duration::from_secs(2), accept_invalid_certs: false, - #[cfg(feature = "release-health")] auto_session_tracking: false, - #[cfg(feature = "release-health")] session_mode: SessionMode::Application, extra_border_frames: vec![], trim_backtraces: true, diff --git a/sentry/src/init.rs b/sentry/src/init.rs index eeee2e3d4..27fdbce29 100644 --- a/sentry/src/init.rs +++ b/sentry/src/init.rs @@ -97,7 +97,9 @@ where { let opts = apply_defaults(opts.into()); + #[allow(unused)] let auto_session_tracking = opts.auto_session_tracking; + #[allow(unused)] let session_mode = opts.session_mode; let client = Arc::new(Client::from(opts)); From e23c8876bfa9847926bab164b6c2faf55fb800b5 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 20 Mar 2025 12:11:03 +0100 Subject: [PATCH 10/13] doc --- sentry-core/src/clientoptions.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 583aae8cc..1be8e3956 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -32,6 +32,8 @@ pub type BeforeCallback = Arc Option + Send + Sync>; /// /// See the [Documentation on Session Modes](https://develop.sentry.dev/sdk/sessions/#sdk-considerations) /// for more information. +/// +/// The `release-health` feature needs to be enabled for this option to have any effect. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SessionMode { /// Long running application session. From df86d8e5dc12485a04dc23a2149137711719789c Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 28 Mar 2025 10:13:10 +0100 Subject: [PATCH 11/13] updates --- sentry-core/src/client.rs | 12 +- sentry-core/src/clientoptions.rs | 9 +- sentry-core/src/scope/real.rs | 19 +- sentry-core/src/session.rs | 1154 +++++++++++++++--------------- 4 files changed, 613 insertions(+), 581 deletions(-) diff --git a/sentry-core/src/client.rs b/sentry-core/src/client.rs index 5f7383237..684f7f2a8 100644 --- a/sentry-core/src/client.rs +++ b/sentry-core/src/client.rs @@ -6,11 +6,13 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; use rand::random; +#[cfg(feature = "release-health")] use sentry_types::protocol::v7::SessionUpdate; use sentry_types::random_uuid; use crate::constants::SDK_INFO; use crate::protocol::{ClientSdkInfo, Event}; +#[cfg(feature = "release-health")] use crate::session::SessionFlusher; use crate::types::{Dsn, Uuid}; #[cfg(feature = "release-health")] @@ -45,6 +47,7 @@ pub(crate) type TransportArc = Arc>>>; pub struct Client { options: ClientOptions, transport: TransportArc, + #[cfg(feature = "release-health")] session_flusher: RwLock>, integrations: Vec<(TypeId, Arc)>, pub(crate) sdk_info: ClientSdkInfo, @@ -68,12 +71,11 @@ impl Clone for Client { transport.clone(), self.options.session_mode, ))); - #[cfg(not(feature = "release-health"))] - let session_flusher = RwLock::new(None); Client { options: self.options.clone(), transport, + #[cfg(feature = "release-health")] session_flusher, integrations: self.integrations.clone(), sdk_info: self.sdk_info.clone(), @@ -143,12 +145,11 @@ impl Client { transport.clone(), options.session_mode, ))); - #[cfg(not(feature = "release-health"))] - let session_flusher = RwLock::new(None); Client { options, transport, + #[cfg(feature = "release-health")] session_flusher, integrations, sdk_info, @@ -311,6 +312,7 @@ impl Client { } } + #[cfg(feature = "release-health")] pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) { if let Some(ref flusher) = *self.session_flusher.read().unwrap() { flusher.enqueue(session_update); @@ -319,6 +321,7 @@ impl Client { /// Drains all pending events without shutting down. pub fn flush(&self, timeout: Option) -> bool { + #[cfg(feature = "release-health")] if let Some(ref flusher) = *self.session_flusher.read().unwrap() { flusher.flush(); } @@ -337,6 +340,7 @@ impl Client { /// If no timeout is provided the client will wait for as long a /// `shutdown_timeout` in the client options. pub fn close(&self, timeout: Option) -> bool { + #[cfg(feature = "release-health")] drop(self.session_flusher.write().unwrap().take()); let transport_opt = self.transport.write().unwrap().take(); if let Some(transport) = transport_opt { diff --git a/sentry-core/src/clientoptions.rs b/sentry-core/src/clientoptions.rs index 1be8e3956..9c0f98d34 100644 --- a/sentry-core/src/clientoptions.rs +++ b/sentry-core/src/clientoptions.rs @@ -33,7 +33,8 @@ pub type BeforeCallback = Arc Option + Send + Sync>; /// See the [Documentation on Session Modes](https://develop.sentry.dev/sdk/sessions/#sdk-considerations) /// for more information. /// -/// The `release-health` feature needs to be enabled for this option to have any effect. +/// **NOTE**: The `release-health` feature (enabled by default) needs to be enabled for this option to have +/// any effect. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SessionMode { /// Long running application session. @@ -173,8 +174,14 @@ pub struct ClientOptions { /// When automatic session tracking is enabled, a new "user-mode" session /// is started at the time of `sentry::init`, and will persist for the /// application lifetime. + /// + /// **NOTE**: The `release-health` feature (enabled by default) needs to be enabled for this option to have + /// any effect. pub auto_session_tracking: bool, /// Determine how Sessions are being tracked. + /// + /// **NOTE**: The `release-health` feature (enabled by default) needs to be enabled for this option to have + /// any effect. pub session_mode: SessionMode, /// Border frames which indicate a border from a backtrace to /// useless internals. Some are automatically included. diff --git a/sentry-core/src/scope/real.rs b/sentry-core/src/scope/real.rs index ed033fb00..7c4eda471 100644 --- a/sentry-core/src/scope/real.rs +++ b/sentry-core/src/scope/real.rs @@ -1,10 +1,13 @@ use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; use std::fmt; -use std::sync::{Arc, Mutex, PoisonError, RwLock}; +#[cfg(feature = "release-health")] +use std::sync::Mutex; +use std::sync::{Arc, PoisonError, RwLock}; use crate::performance::TransactionOrSpan; use crate::protocol::{Attachment, Breadcrumb, Context, Event, Level, Transaction, User, Value}; +#[cfg(feature = "release-health")] use crate::session::Session; use crate::Client; @@ -45,6 +48,7 @@ pub struct Scope { pub(crate) tags: Arc>, pub(crate) contexts: Arc>, pub(crate) event_processors: Arc>, + #[cfg(feature = "release-health")] pub(crate) session: Arc>>, pub(crate) span: Arc>, pub(crate) attachments: Arc>, @@ -52,7 +56,8 @@ pub struct Scope { impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Scope") + let mut debug_struct = f.debug_struct("Scope"); + debug_struct .field("level", &self.level) .field("fingerprint", &self.fingerprint) .field("transaction", &self.transaction) @@ -61,8 +66,12 @@ impl fmt::Debug for Scope { .field("extra", &self.extra) .field("tags", &self.tags) .field("contexts", &self.contexts) - .field("event_processors", &self.event_processors.len()) - .field("session", &self.session) + .field("event_processors", &self.event_processors.len()); + + #[cfg(feature = "release-health")] + debug_struct.field("session", &self.session); + + debug_struct .field("span", &self.span) .field("attachments", &self.attachments.len()) .finish() @@ -341,7 +350,9 @@ impl Scope { self.span.as_ref().clone() } + #[allow(unused_variables)] pub(crate) fn update_session_from_event(&self, event: &Event<'static>) { + #[cfg(feature = "release-health")] if let Some(session) = self.session.lock().unwrap().as_mut() { session.update_from_event(event); } diff --git a/sentry-core/src/session.rs b/sentry-core/src/session.rs index c7c8d1bcb..dd7a0948d 100644 --- a/sentry-core/src/session.rs +++ b/sentry-core/src/session.rs @@ -2,674 +2,684 @@ //! //! -use std::collections::HashMap; -use std::sync::{Arc, Condvar, Mutex, MutexGuard}; -use std::thread::JoinHandle; -use std::time::{Duration, Instant, SystemTime}; - -use crate::client::TransportArc; -use crate::clientoptions::SessionMode; -use crate::protocol::{ - EnvelopeItem, Event, Level, SessionAggregateItem, SessionAggregates, SessionAttributes, - SessionStatus, SessionUpdate, -}; - #[cfg(feature = "release-health")] -use crate::scope::StackLayer; +pub use session::*; #[cfg(feature = "release-health")] -use crate::types::random_uuid; -use crate::{Client, Envelope}; - -#[derive(Clone, Debug)] -pub struct Session { - client: Arc, - session_update: SessionUpdate<'static>, - started: Instant, - dirty: bool, -} +mod session { -impl Drop for Session { - fn drop(&mut self) { - self.close(SessionStatus::Exited); - if self.dirty { - self.client.enqueue_session(self.session_update.clone()); - } - } -} + use std::collections::HashMap; + use std::sync::{Arc, Condvar, Mutex, MutexGuard}; + use std::thread::JoinHandle; + use std::time::{Duration, Instant, SystemTime}; + + use crate::client::TransportArc; + use crate::clientoptions::SessionMode; + use crate::protocol::{ + EnvelopeItem, Event, Level, SessionAggregateItem, SessionAggregates, SessionAttributes, + SessionStatus, SessionUpdate, + }; -impl Session { #[cfg(feature = "release-health")] - pub fn from_stack(stack: &StackLayer) -> Option { - let client = stack.client.as_ref()?; - let options = client.options(); - let user = stack.scope.user.as_deref(); - let distinct_id = user - .and_then(|user| { - user.id - .as_ref() - .or(user.email.as_ref()) - .or(user.username.as_ref()) - }) - .cloned(); - Some(Self { - client: client.clone(), - session_update: SessionUpdate { - session_id: random_uuid(), - distinct_id, - sequence: None, - timestamp: None, - started: SystemTime::now(), - init: true, - duration: None, - status: SessionStatus::Ok, - errors: 0, - attributes: SessionAttributes { - release: options.release.clone()?, - environment: options.environment.clone(), - ip_address: None, - user_agent: None, - }, - }, - started: Instant::now(), - dirty: true, - }) + use crate::scope::StackLayer; + + #[cfg(feature = "release-health")] + use crate::types::random_uuid; + use crate::{Client, Envelope}; + + #[derive(Clone, Debug)] + pub struct Session { + client: Arc, + session_update: SessionUpdate<'static>, + started: Instant, + dirty: bool, } - pub(crate) fn update_from_event(&mut self, event: &Event<'static>) { - if self.session_update.status != SessionStatus::Ok { - // a session that has already transitioned to a "terminal" state - // should not receive any more updates - return; - } - let mut has_error = event.level >= Level::Error; - let mut is_crash = false; - for exc in &event.exception.values { - has_error = true; - if let Some(mechanism) = &exc.mechanism { - if let Some(false) = mechanism.handled { - is_crash = true; - break; - } + impl Drop for Session { + fn drop(&mut self) { + self.close(SessionStatus::Exited); + if self.dirty { + self.client.enqueue_session(self.session_update.clone()); } } + } - if is_crash { - self.session_update.status = SessionStatus::Crashed; + impl Session { + #[cfg(feature = "release-health")] + pub fn from_stack(stack: &StackLayer) -> Option { + let client = stack.client.as_ref()?; + let options = client.options(); + let user = stack.scope.user.as_deref(); + let distinct_id = user + .and_then(|user| { + user.id + .as_ref() + .or(user.email.as_ref()) + .or(user.username.as_ref()) + }) + .cloned(); + Some(Self { + client: client.clone(), + session_update: SessionUpdate { + session_id: random_uuid(), + distinct_id, + sequence: None, + timestamp: None, + started: SystemTime::now(), + init: true, + duration: None, + status: SessionStatus::Ok, + errors: 0, + attributes: SessionAttributes { + release: options.release.clone()?, + environment: options.environment.clone(), + ip_address: None, + user_agent: None, + }, + }, + started: Instant::now(), + dirty: true, + }) } - if has_error { - self.session_update.errors += 1; - self.dirty = true; + + pub(crate) fn update_from_event(&mut self, event: &Event<'static>) { + if self.session_update.status != SessionStatus::Ok { + // a session that has already transitioned to a "terminal" state + // should not receive any more updates + return; + } + let mut has_error = event.level >= Level::Error; + let mut is_crash = false; + for exc in &event.exception.values { + has_error = true; + if let Some(mechanism) = &exc.mechanism { + if let Some(false) = mechanism.handled { + is_crash = true; + break; + } + } + } + + if is_crash { + self.session_update.status = SessionStatus::Crashed; + } + if has_error { + self.session_update.errors += 1; + self.dirty = true; + } } - } - pub(crate) fn close(&mut self, status: SessionStatus) { - if self.session_update.status == SessionStatus::Ok { - let status = match status { - SessionStatus::Ok => SessionStatus::Exited, - s => s, - }; - self.session_update.duration = Some(self.started.elapsed().as_secs_f64()); - self.session_update.status = status; - self.dirty = true; + pub(crate) fn close(&mut self, status: SessionStatus) { + if self.session_update.status == SessionStatus::Ok { + let status = match status { + SessionStatus::Ok => SessionStatus::Exited, + s => s, + }; + self.session_update.duration = Some(self.started.elapsed().as_secs_f64()); + self.session_update.status = status; + self.dirty = true; + } } - } - #[cfg(feature = "release-health")] - pub(crate) fn create_envelope_item(&mut self) -> Option { - if self.dirty { - let item = self.session_update.clone().into(); - self.session_update.init = false; - self.dirty = false; - return Some(item); + #[cfg(feature = "release-health")] + pub(crate) fn create_envelope_item(&mut self) -> Option { + if self.dirty { + let item = self.session_update.clone().into(); + self.session_update.init = false; + self.dirty = false; + return Some(item); + } + None } - None } -} - -// as defined here: https://develop.sentry.dev/sdk/envelopes/#size-limits -const MAX_SESSION_ITEMS: usize = 100; -#[cfg(feature = "release-health")] -const FLUSH_INTERVAL: Duration = Duration::from_secs(60); -#[derive(Debug, Default)] -struct SessionQueue { - individual: Vec>, - aggregated: Option, -} + // as defined here: https://develop.sentry.dev/sdk/envelopes/#size-limits + const MAX_SESSION_ITEMS: usize = 100; + #[cfg(feature = "release-health")] + const FLUSH_INTERVAL: Duration = Duration::from_secs(60); -#[derive(Debug)] -struct AggregatedSessions { - buckets: HashMap, - attributes: SessionAttributes<'static>, -} + #[derive(Debug, Default)] + struct SessionQueue { + individual: Vec>, + aggregated: Option, + } -impl From for EnvelopeItem { - fn from(sessions: AggregatedSessions) -> Self { - let aggregates = sessions - .buckets - .into_iter() - .map(|(key, counts)| SessionAggregateItem { - started: key.started, - distinct_id: key.distinct_id, - exited: counts.exited, - errored: counts.errored, - abnormal: counts.abnormal, - crashed: counts.crashed, - }) - .collect(); + #[derive(Debug)] + struct AggregatedSessions { + buckets: HashMap, + attributes: SessionAttributes<'static>, + } - SessionAggregates { - aggregates, - attributes: sessions.attributes, + impl From for EnvelopeItem { + fn from(sessions: AggregatedSessions) -> Self { + let aggregates = sessions + .buckets + .into_iter() + .map(|(key, counts)| SessionAggregateItem { + started: key.started, + distinct_id: key.distinct_id, + exited: counts.exited, + errored: counts.errored, + abnormal: counts.abnormal, + crashed: counts.crashed, + }) + .collect(); + + SessionAggregates { + aggregates, + attributes: sessions.attributes, + } + .into() } - .into() } -} -#[derive(Debug, PartialEq, Eq, Hash)] -struct AggregationKey { - started: SystemTime, - distinct_id: Option, -} + #[derive(Debug, PartialEq, Eq, Hash)] + struct AggregationKey { + started: SystemTime, + distinct_id: Option, + } -#[derive(Debug, Default)] -struct AggregationCounts { - exited: u32, - errored: u32, - abnormal: u32, - crashed: u32, -} + #[derive(Debug, Default)] + struct AggregationCounts { + exited: u32, + errored: u32, + abnormal: u32, + crashed: u32, + } -/// Background Session Flusher -/// -/// The background flusher queues session updates for delayed batched sending. -/// It has its own background thread that will flush its queue once every -/// `FLUSH_INTERVAL`. -pub(crate) struct SessionFlusher { - transport: TransportArc, - mode: SessionMode, - queue: Arc>, - shutdown: Arc<(Mutex, Condvar)>, - worker: Option>, -} + /// Background Session Flusher + /// + /// The background flusher queues session updates for delayed batched sending. + /// It has its own background thread that will flush its queue once every + /// `FLUSH_INTERVAL`. + pub(crate) struct SessionFlusher { + transport: TransportArc, + mode: SessionMode, + queue: Arc>, + shutdown: Arc<(Mutex, Condvar)>, + worker: Option>, + } -impl SessionFlusher { - /// Creates a new Flusher that will submit envelopes to the given `transport`. - #[cfg(feature = "release-health")] - pub fn new(transport: TransportArc, mode: SessionMode) -> Self { - let queue = Arc::new(Mutex::new(Default::default())); - #[allow(clippy::mutex_atomic)] - let shutdown = Arc::new((Mutex::new(false), Condvar::new())); - - let worker_transport = transport.clone(); - let worker_queue = queue.clone(); - let worker_shutdown = shutdown.clone(); - let worker = std::thread::Builder::new() - .name("sentry-session-flusher".into()) - .spawn(move || { - let (lock, cvar) = worker_shutdown.as_ref(); - let mut shutdown = lock.lock().unwrap(); - // check this immediately, in case the main thread is already shutting down - if *shutdown { - return; - } - let mut last_flush = Instant::now(); - loop { - let timeout = FLUSH_INTERVAL - .checked_sub(last_flush.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; + impl SessionFlusher { + /// Creates a new Flusher that will submit envelopes to the given `transport`. + #[cfg(feature = "release-health")] + pub fn new(transport: TransportArc, mode: SessionMode) -> Self { + let queue = Arc::new(Mutex::new(Default::default())); + #[allow(clippy::mutex_atomic)] + let shutdown = Arc::new((Mutex::new(false), Condvar::new())); + + let worker_transport = transport.clone(); + let worker_queue = queue.clone(); + let worker_shutdown = shutdown.clone(); + let worker = std::thread::Builder::new() + .name("sentry-session-flusher".into()) + .spawn(move || { + let (lock, cvar) = worker_shutdown.as_ref(); + let mut shutdown = lock.lock().unwrap(); + // check this immediately, in case the main thread is already shutting down if *shutdown { return; } - if last_flush.elapsed() < FLUSH_INTERVAL { - continue; + let mut last_flush = Instant::now(); + loop { + let timeout = FLUSH_INTERVAL + .checked_sub(last_flush.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; + if *shutdown { + return; + } + if last_flush.elapsed() < FLUSH_INTERVAL { + continue; + } + SessionFlusher::flush_queue_internal( + worker_queue.lock().unwrap(), + &worker_transport, + ); + last_flush = Instant::now(); } - SessionFlusher::flush_queue_internal( - worker_queue.lock().unwrap(), - &worker_transport, - ); - last_flush = Instant::now(); - } - }) - .unwrap(); - - Self { - transport, - mode, - queue, - shutdown, - worker: Some(worker), - } - } - - /// Enqueues a session update for delayed sending. - /// - /// This will aggregate session counts in request mode, for all sessions - /// that were not yet partially sent. - pub fn enqueue(&self, session_update: SessionUpdate<'static>) { - let mut queue = self.queue.lock().unwrap(); - if self.mode == SessionMode::Application || !session_update.init { - queue.individual.push(session_update); - if queue.individual.len() >= MAX_SESSION_ITEMS { - SessionFlusher::flush_queue_internal(queue, &self.transport); + }) + .unwrap(); + + Self { + transport, + mode, + queue, + shutdown, + worker: Some(worker), } - return; } - let aggregate = queue.aggregated.get_or_insert_with(|| AggregatedSessions { - buckets: HashMap::with_capacity(1), - attributes: session_update.attributes.clone(), - }); - - let duration = session_update - .started - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - let duration = (duration.as_secs() / 60) * 60; - let started = SystemTime::UNIX_EPOCH - .checked_add(Duration::from_secs(duration)) - .unwrap(); - - let key = AggregationKey { - started, - distinct_id: session_update.distinct_id, - }; - - let bucket = aggregate.buckets.entry(key).or_default(); - - match session_update.status { - SessionStatus::Exited => { - if session_update.errors > 0 { - bucket.errored += 1; - } else { - bucket.exited += 1; + /// Enqueues a session update for delayed sending. + /// + /// This will aggregate session counts in request mode, for all sessions + /// that were not yet partially sent. + pub fn enqueue(&self, session_update: SessionUpdate<'static>) { + let mut queue = self.queue.lock().unwrap(); + if self.mode == SessionMode::Application || !session_update.init { + queue.individual.push(session_update); + if queue.individual.len() >= MAX_SESSION_ITEMS { + SessionFlusher::flush_queue_internal(queue, &self.transport); } + return; } - SessionStatus::Crashed => { - bucket.crashed += 1; - } - SessionStatus::Abnormal => { - bucket.abnormal += 1; - } - SessionStatus::Ok => { - sentry_debug!("unreachable: only closed sessions will be enqueued"); - } - } - } - /// Flushes the queue to the transport. - pub fn flush(&self) { - let queue = self.queue.lock().unwrap(); - SessionFlusher::flush_queue_internal(queue, &self.transport); - } + let aggregate = queue.aggregated.get_or_insert_with(|| AggregatedSessions { + buckets: HashMap::with_capacity(1), + attributes: session_update.attributes.clone(), + }); - /// Flushes the queue to the transport. - /// - /// This is a static method as it will be called from both the background - /// thread and the main thread on drop. - fn flush_queue_internal(mut queue_lock: MutexGuard, transport: &TransportArc) { - let queue = std::mem::take(&mut queue_lock.individual); - let aggregate = queue_lock.aggregated.take(); - drop(queue_lock); - - // send aggregates - if let Some(aggregate) = aggregate { - if let Some(ref transport) = *transport.read().unwrap() { - let mut envelope = Envelope::new(); - envelope.add_item(aggregate); - transport.send_envelope(envelope); + let duration = session_update + .started + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let duration = (duration.as_secs() / 60) * 60; + let started = SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs(duration)) + .unwrap(); + + let key = AggregationKey { + started, + distinct_id: session_update.distinct_id, + }; + + let bucket = aggregate.buckets.entry(key).or_default(); + + match session_update.status { + SessionStatus::Exited => { + if session_update.errors > 0 { + bucket.errored += 1; + } else { + bucket.exited += 1; + } + } + SessionStatus::Crashed => { + bucket.crashed += 1; + } + SessionStatus::Abnormal => { + bucket.abnormal += 1; + } + SessionStatus::Ok => { + sentry_debug!("unreachable: only closed sessions will be enqueued"); + } } } - // send individual items - if queue.is_empty() { - return; + /// Flushes the queue to the transport. + pub fn flush(&self) { + let queue = self.queue.lock().unwrap(); + SessionFlusher::flush_queue_internal(queue, &self.transport); } - let mut envelope = Envelope::new(); - let mut items = 0; - - for session_update in queue { - if items >= MAX_SESSION_ITEMS { + /// Flushes the queue to the transport. + /// + /// This is a static method as it will be called from both the background + /// thread and the main thread on drop. + fn flush_queue_internal( + mut queue_lock: MutexGuard, + transport: &TransportArc, + ) { + let queue = std::mem::take(&mut queue_lock.individual); + let aggregate = queue_lock.aggregated.take(); + drop(queue_lock); + + // send aggregates + if let Some(aggregate) = aggregate { if let Some(ref transport) = *transport.read().unwrap() { + let mut envelope = Envelope::new(); + envelope.add_item(aggregate); transport.send_envelope(envelope); } - envelope = Envelope::new(); - items = 0; } - envelope.add_item(session_update); - items += 1; - } + // send individual items + if queue.is_empty() { + return; + } - if let Some(ref transport) = *transport.read().unwrap() { - transport.send_envelope(envelope); - } - } -} + let mut envelope = Envelope::new(); + let mut items = 0; + + for session_update in queue { + if items >= MAX_SESSION_ITEMS { + if let Some(ref transport) = *transport.read().unwrap() { + transport.send_envelope(envelope); + } + envelope = Envelope::new(); + items = 0; + } -impl Drop for SessionFlusher { - fn drop(&mut self) { - let (lock, cvar) = self.shutdown.as_ref(); - *lock.lock().unwrap() = true; - cvar.notify_one(); + envelope.add_item(session_update); + items += 1; + } - if let Some(worker) = self.worker.take() { - worker.join().ok(); + if let Some(ref transport) = *transport.read().unwrap() { + transport.send_envelope(envelope); + } } - SessionFlusher::flush_queue_internal(self.queue.lock().unwrap(), &self.transport); } -} -#[cfg(all(test, feature = "test"))] -mod tests { - use std::cmp::Ordering; - - use super::*; - use crate as sentry; - use crate::protocol::{Envelope, EnvelopeItem, SessionStatus}; - - fn capture_envelopes(f: F) -> Vec - where - F: FnOnce(), - { - crate::test::with_captured_envelopes_options( - f, - crate::ClientOptions { - release: Some("some-release".into()), - ..Default::default() - }, - ) - } + impl Drop for SessionFlusher { + fn drop(&mut self) { + let (lock, cvar) = self.shutdown.as_ref(); + *lock.lock().unwrap() = true; + cvar.notify_one(); - #[test] - fn test_session_startstop() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); - std::thread::sleep(std::time::Duration::from_millis(10)); - }); - assert_eq!(envelopes.len(), 1); - - let mut items = envelopes[0].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert!(session.duration.unwrap() > 0.01); - assert_eq!(session.errors, 0); - assert_eq!(session.attributes.release, "some-release"); - assert!(session.init); - } else { - panic!("expected session"); + if let Some(worker) = self.worker.take() { + worker.join().ok(); + } + SessionFlusher::flush_queue_internal(self.queue.lock().unwrap(), &self.transport); } - assert_eq!(items.next(), None); } - #[test] - fn test_session_batching() { - let envelopes = capture_envelopes(|| { - for _ in 0..(MAX_SESSION_ITEMS * 2) { - sentry::start_session(); - } - }); - // we only want *two* envelope for all the sessions - assert_eq!(envelopes.len(), 2); - - let items = envelopes[0].items().chain(envelopes[1].items()); - assert_eq!(items.clone().count(), MAX_SESSION_ITEMS * 2); - for item in items { - assert!(matches!(item, EnvelopeItem::SessionUpdate(_))); + #[cfg(all(test, feature = "test"))] + mod tests { + use std::cmp::Ordering; + + use super::*; + use crate as sentry; + use crate::protocol::{Envelope, EnvelopeItem, SessionStatus}; + + fn capture_envelopes(f: F) -> Vec + where + F: FnOnce(), + { + crate::test::with_captured_envelopes_options( + f, + crate::ClientOptions { + release: Some("some-release".into()), + ..Default::default() + }, + ) } - } - #[test] - fn test_session_aggregation() { - let envelopes = crate::test::with_captured_envelopes_options( - || { + #[test] + fn test_session_startstop() { + let envelopes = capture_envelopes(|| { sentry::start_session(); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); + std::thread::sleep(std::time::Duration::from_millis(10)); + }); + assert_eq!(envelopes.len(), 1); + + let mut items = envelopes[0].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert!(session.duration.unwrap() > 0.01); + assert_eq!(session.errors, 0); + assert_eq!(session.attributes.release, "some-release"); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + } - for _ in 0..50 { + #[test] + fn test_session_batching() { + let envelopes = capture_envelopes(|| { + for _ in 0..(MAX_SESSION_ITEMS * 2) { sentry::start_session(); } - sentry::end_session(); - - sentry::configure_scope(|scope| { - scope.set_user(Some(sentry::User { - id: Some("foo-bar".into()), - ..Default::default() - })); - scope.add_event_processor(Box::new(|_| None)); - }); + }); + // we only want *two* envelope for all the sessions + assert_eq!(envelopes.len(), 2); - for _ in 0..50 { + let items = envelopes[0].items().chain(envelopes[1].items()); + assert_eq!(items.clone().count(), MAX_SESSION_ITEMS * 2); + for item in items { + assert!(matches!(item, EnvelopeItem::SessionUpdate(_))); + } + } + + #[test] + fn test_session_aggregation() { + let envelopes = crate::test::with_captured_envelopes_options( + || { sentry::start_session(); - } + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); - // This error will be discarded because of the event processor, - // and session will not be updated. - // Only events dropped due to sampling should update the session. - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }, - crate::ClientOptions { - release: Some("some-release".into()), - #[cfg(feature = "release-health")] - session_mode: SessionMode::Request, - ..Default::default() - }, - ); - assert_eq!(envelopes.len(), 2); - - let mut items = envelopes[0].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - assert_eq!(items.next(), None); - - let mut items = envelopes[1].items(); - if let Some(EnvelopeItem::SessionAggregates(aggregate)) = items.next() { - let mut aggregates = aggregate.aggregates.clone(); - assert_eq!(aggregates.len(), 2); - // the order depends on a hashmap and is not stable otherwise - aggregates.sort_by(|a, b| { - a.distinct_id - .partial_cmp(&b.distinct_id) - .unwrap_or(Ordering::Less) - }); + for _ in 0..50 { + sentry::start_session(); + } + sentry::end_session(); - assert_eq!(aggregates[0].distinct_id, None); - assert_eq!(aggregates[0].exited, 50); + sentry::configure_scope(|scope| { + scope.set_user(Some(sentry::User { + id: Some("foo-bar".into()), + ..Default::default() + })); + scope.add_event_processor(Box::new(|_| None)); + }); - assert_eq!(aggregates[1].errored, 0); - assert_eq!(aggregates[1].distinct_id, Some("foo-bar".into())); - assert_eq!(aggregates[1].exited, 50); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - } + for _ in 0..50 { + sentry::start_session(); + } - #[test] - fn test_session_error() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); - - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }); - assert_eq!(envelopes.len(), 2); - - let mut items = envelopes[0].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Ok); - assert_eq!(session.errors, 1); - assert_eq!(session.attributes.release, "some-release"); - assert!(session.init); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - - let mut items = envelopes[1].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 1); - assert!(!session.init); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - } + // This error will be discarded because of the event processor, + // and session will not be updated. + // Only events dropped due to sampling should update the session. + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }, + crate::ClientOptions { + release: Some("some-release".into()), + #[cfg(feature = "release-health")] + session_mode: SessionMode::Request, + ..Default::default() + }, + ); + assert_eq!(envelopes.len(), 2); + + let mut items = envelopes[0].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + assert_eq!(items.next(), None); + + let mut items = envelopes[1].items(); + if let Some(EnvelopeItem::SessionAggregates(aggregate)) = items.next() { + let mut aggregates = aggregate.aggregates.clone(); + assert_eq!(aggregates.len(), 2); + // the order depends on a hashmap and is not stable otherwise + aggregates.sort_by(|a, b| { + a.distinct_id + .partial_cmp(&b.distinct_id) + .unwrap_or(Ordering::Less) + }); + + assert_eq!(aggregates[0].distinct_id, None); + assert_eq!(aggregates[0].exited, 50); - #[test] - fn test_session_abnormal() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); - sentry::end_session_with_status(SessionStatus::Abnormal); - }); - assert_eq!(envelopes.len(), 1); - - let mut items = envelopes[0].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Abnormal); - assert!(session.init); - } else { - panic!("expected session"); + assert_eq!(aggregates[1].errored, 0); + assert_eq!(aggregates[1].distinct_id, Some("foo-bar".into())); + assert_eq!(aggregates[1].exited, 50); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); - } - #[test] - fn test_session_sampled_errors() { - let mut envelopes = crate::test::with_captured_envelopes_options( - || { + #[test] + fn test_session_error() { + let envelopes = capture_envelopes(|| { sentry::start_session(); - for _ in 0..100 { - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - } - }, - crate::ClientOptions { - release: Some("some-release".into()), - sample_rate: 0.5, - ..Default::default() - }, - ); - assert!(envelopes.len() > 25); - assert!(envelopes.len() < 75); - - let envelope = envelopes.pop().unwrap(); - let mut items = envelope.items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 100); - } else { - panic!("expected session"); + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }); + assert_eq!(envelopes.len(), 2); + + let mut items = envelopes[0].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Ok); + assert_eq!(session.errors, 1); + assert_eq!(session.attributes.release, "some-release"); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + + let mut items = envelopes[1].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 1); + assert!(!session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + } + + #[test] + fn test_session_abnormal() { + let envelopes = capture_envelopes(|| { + sentry::start_session(); + sentry::end_session_with_status(SessionStatus::Abnormal); + }); + assert_eq!(envelopes.len(), 1); + + let mut items = envelopes[0].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Abnormal); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); - } - /// For _user-mode_ sessions, we want to inherit the session for any _new_ - /// Hub that is spawned from the main thread Hub which already has a session - /// attached - #[test] - fn test_inherit_session_from_top() { - let envelopes = capture_envelopes(|| { - sentry::start_session(); + #[test] + fn test_session_sampled_errors() { + let mut envelopes = crate::test::with_captured_envelopes_options( + || { + sentry::start_session(); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); + for _ in 0..100 { + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + } + }, + crate::ClientOptions { + release: Some("some-release".into()), + sample_rate: 0.5, + ..Default::default() + }, + ); + assert!(envelopes.len() > 25); + assert!(envelopes.len() < 75); + + let envelope = envelopes.pop().unwrap(); + let mut items = envelope.items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 100); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + } - // create a new Hub which should have the same session - let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); + /// For _user-mode_ sessions, we want to inherit the session for any _new_ + /// Hub that is spawned from the main thread Hub which already has a session + /// attached + #[test] + fn test_inherit_session_from_top() { + let envelopes = capture_envelopes(|| { + sentry::start_session(); - sentry::Hub::run(hub, || { let err = "NaN".parse::().unwrap_err(); sentry::capture_error(&err); - sentry::with_scope( - |_| {}, - || { - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }, - ); + // create a new Hub which should have the same session + let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); + + sentry::Hub::run(hub, || { + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + + sentry::with_scope( + |_| {}, + || { + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }, + ); + }); }); - }); - assert_eq!(envelopes.len(), 4); // 3 errors and one session end + assert_eq!(envelopes.len(), 4); // 3 errors and one session end - let mut items = envelopes[3].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 3); - assert!(!session.init); - } else { - panic!("expected session"); + let mut items = envelopes[3].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 3); + assert!(!session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); - } - /// We want to forward-inherit sessions as the previous test asserted, but - /// not *backwards*. So any new session created in a derived Hub and scope - /// will only get updates from that particular scope. - #[test] - fn test_dont_inherit_session_backwards() { - let envelopes = capture_envelopes(|| { - let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); - - sentry::Hub::run(hub, || { - sentry::with_scope( - |_| {}, - || { - sentry::start_session(); + /// We want to forward-inherit sessions as the previous test asserted, but + /// not *backwards*. So any new session created in a derived Hub and scope + /// will only get updates from that particular scope. + #[test] + fn test_dont_inherit_session_backwards() { + let envelopes = capture_envelopes(|| { + let hub = std::sync::Arc::new(sentry::Hub::new_from_top(sentry::Hub::current())); + + sentry::Hub::run(hub, || { + sentry::with_scope( + |_| {}, + || { + sentry::start_session(); + + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }, + ); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }, - ); + let err = "NaN".parse::().unwrap_err(); + sentry::capture_error(&err); + }); let err = "NaN".parse::().unwrap_err(); sentry::capture_error(&err); }); - let err = "NaN".parse::().unwrap_err(); - sentry::capture_error(&err); - }); - - assert_eq!(envelopes.len(), 4); // 3 errors and one session end + assert_eq!(envelopes.len(), 4); // 3 errors and one session end - let mut items = envelopes[0].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Ok); - assert_eq!(session.errors, 1); - assert!(session.init); - } else { - panic!("expected session"); - } - assert_eq!(items.next(), None); - - // the other two events should not have session updates - let mut items = envelopes[1].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - assert_eq!(items.next(), None); - - let mut items = envelopes[2].items(); - assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); - assert_eq!(items.next(), None); - - // the session end is sent last as it is possibly batched - let mut items = envelopes[3].items(); - if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { - assert_eq!(session.status, SessionStatus::Exited); - assert_eq!(session.errors, 1); - assert!(!session.init); - } else { - panic!("expected session"); + let mut items = envelopes[0].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Ok); + assert_eq!(session.errors, 1); + assert!(session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); + + // the other two events should not have session updates + let mut items = envelopes[1].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + assert_eq!(items.next(), None); + + let mut items = envelopes[2].items(); + assert!(matches!(items.next(), Some(EnvelopeItem::Event(_)))); + assert_eq!(items.next(), None); + + // the session end is sent last as it is possibly batched + let mut items = envelopes[3].items(); + if let Some(EnvelopeItem::SessionUpdate(session)) = items.next() { + assert_eq!(session.status, SessionStatus::Exited); + assert_eq!(session.errors, 1); + assert!(!session.init); + } else { + panic!("expected session"); + } + assert_eq!(items.next(), None); } - assert_eq!(items.next(), None); } } From 1000b10866886ac80fa82eb0cd0a76406438e1e6 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 28 Mar 2025 10:16:46 +0100 Subject: [PATCH 12/13] rename module --- sentry-core/src/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-core/src/session.rs b/sentry-core/src/session.rs index dd7a0948d..00739872f 100644 --- a/sentry-core/src/session.rs +++ b/sentry-core/src/session.rs @@ -3,10 +3,10 @@ //! #[cfg(feature = "release-health")] -pub use session::*; +pub use session_impl::*; #[cfg(feature = "release-health")] -mod session { +mod session_impl { use std::collections::HashMap; use std::sync::{Arc, Condvar, Mutex, MutexGuard}; From fa07d6e0e0bc0e8c225376d3c10faf1ad4d9b846 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 28 Mar 2025 11:23:44 +0100 Subject: [PATCH 13/13] enable by default in subcrates --- sentry-actix/Cargo.toml | 5 ++++- sentry-tower/Cargo.toml | 2 ++ sentry-tracing/Cargo.toml | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sentry-actix/Cargo.toml b/sentry-actix/Cargo.toml index 12a77d944..a1238db10 100644 --- a/sentry-actix/Cargo.toml +++ b/sentry-actix/Cargo.toml @@ -12,12 +12,15 @@ Sentry client extension for actix-web 3. edition = "2021" rust-version = "1.75" +[features] +default = ["release-health"] +release-health = ["sentry-core/release-health"] + [dependencies] actix-web = { version = "4", default-features = false } futures-util = { version = "0.3.5", default-features = false } sentry-core = { version = "0.36.0", path = "../sentry-core", default-features = false, features = [ "client", - "release-health", ] } actix-http = "3.9.0" diff --git a/sentry-tower/Cargo.toml b/sentry-tower/Cargo.toml index 175a72814..58c915582 100644 --- a/sentry-tower/Cargo.toml +++ b/sentry-tower/Cargo.toml @@ -16,8 +16,10 @@ rust-version = "1.75" all-features = true [features] +default = ["release-health"] http = ["dep:http", "pin-project", "url"] axum-matched-path = ["http", "axum/matched-path"] +release-health = ["sentry-core/release-health"] [dependencies] axum = { version = "0.8", optional = true, default-features = false } diff --git a/sentry-tracing/Cargo.toml b/sentry-tracing/Cargo.toml index ffa7054a1..9097e2d95 100644 --- a/sentry-tracing/Cargo.toml +++ b/sentry-tracing/Cargo.toml @@ -16,8 +16,9 @@ rust-version = "1.75" all-features = true [features] -default = [] +default = ["release-health"] backtrace = ["dep:sentry-backtrace"] +release-health = ["sentry-core/release-health"] [dependencies] sentry-core = { version = "0.36.0", path = "../sentry-core", features = [