From b16d9c3e05a59df2aa4f60c012c7e6ec22b70ae8 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 22 Jul 2025 14:57:07 +0200 Subject: [PATCH 01/28] feat(types): add all the missing supported envelope headers --- sentry-types/src/protocol/envelope.rs | 150 +++++++++++++++++++++----- sentry-types/src/protocol/v7.rs | 66 +++++++++++- sentry-types/src/utils.rs | 32 ++++++ sentry/src/transports/thread.rs | 1 + sentry/src/transports/tokio_thread.rs | 1 + 5 files changed, 220 insertions(+), 30 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 190c439b2..925686336 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1,10 +1,13 @@ -use std::{io::Write, path::Path}; +use std::{io::Write, path::Path, time::SystemTime}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; -use super::v7 as protocol; +use crate::utils::ts_rfc3339_opt; +use crate::Dsn; + +use super::v7::{self as protocol, ClientSdkInfo, DynamicSamplingContext}; use protocol::{ Attachment, AttachmentType, Event, Log, MonitorCheckIn, SessionAggregates, SessionUpdate, @@ -37,9 +40,80 @@ pub enum EnvelopeError { InvalidItemPayload(#[source] serde_json::Error), } -#[derive(Deserialize)] -struct EnvelopeHeader { +/// The supported [Sentry Envelope Headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers). +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct EnvelopeHeaders { + #[serde(default, skip_serializing_if = "Option::is_none")] event_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + dsn: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + sdk: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "ts_rfc3339_opt" + )] + sent_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + trace: Option, +} + +impl EnvelopeHeaders { + /// Returns the Envelope's Event ID, matching the Event ID of the contained event/transaction, + /// if any. + pub fn event_id(&self) -> Option<&Uuid> { + self.event_id.as_ref() + } + + /// Sets the Event ID. + pub fn set_event_id(&mut self, event_id: Option) { + self.event_id = event_id; + } + + /// Returns the DSN. + pub fn dsn(&self) -> Option<&Dsn> { + self.dsn.as_ref() + } + + /// Sets the DSN. + pub fn set_dsn(&mut self, dsn: Option) { + self.dsn = dsn; + } + + /// Returns the SDK information. + pub fn sdk(&self) -> Option<&ClientSdkInfo> { + self.sdk.as_ref() + } + + /// Sets the SDK information. + pub fn set_sdk(&mut self, sdk: Option) { + self.sdk = sdk; + } + + /// Returns the time this envelope was sent. + pub fn sent_at(&self) -> Option<&SystemTime> { + self.sent_at.as_ref() + } + + /// Sets the time this envelope was sent. + /// The value of this timestamp should be generated as close as possible to the transmission of + /// the event. + /// If offline caching is implemented, the SDK should avoid writing this value when envelopes + /// are saved to disk. + pub fn set_sent_at(&mut self, sent_at: Option) { + self.sent_at = sent_at; + } + + /// Returns the Dynamic Sampling Context. + pub fn trace(&self) -> Option<&DynamicSamplingContext> { + self.trace.as_ref() + } + + /// Sets the Dynamic Sampling Context. + pub fn set_trace(&mut self, trace: Option) { + self.trace = trace; + } } /// An Envelope Item Type. @@ -271,7 +345,7 @@ impl Items { /// for more details. #[derive(Clone, Default, Debug, PartialEq)] pub struct Envelope { - event_id: Option, + headers: EnvelopeHeaders, items: Items, } @@ -297,11 +371,11 @@ impl Envelope { return; }; - if self.event_id.is_none() { + if self.headers.event_id.is_none() { if let EnvelopeItem::Event(ref event) = item { - self.event_id = Some(event.event_id); + self.headers.event_id = Some(event.event_id); } else if let EnvelopeItem::Transaction(ref transaction) = item { - self.event_id = Some(transaction.event_id); + self.headers.event_id = Some(transaction.event_id); } } items.push(item); @@ -317,9 +391,19 @@ impl Envelope { EnvelopeItemIter { inner } } + /// Returns a reference to the Envelope's headers. + pub fn headers(&self) -> &EnvelopeHeaders { + &self.headers + } + + /// Sets the Envelope's headers. + pub fn set_headers(&mut self, headers: EnvelopeHeaders) { + self.headers = headers; + } + /// Returns the Envelopes Uuid, if any. pub fn uuid(&self) -> Option<&Uuid> { - self.event_id.as_ref() + self.headers.event_id.as_ref() } /// Returns the [`Event`] contained in this Envelope, if any. @@ -391,11 +475,8 @@ impl Envelope { }; // write the headers: - let event_id = self.uuid(); - match event_id { - Some(uuid) => writeln!(writer, r#"{{"event_id":"{uuid}"}}"#)?, - _ => writeln!(writer, "{{}}")?, - } + serde_json::to_writer(&mut writer, &self.headers)?; + writer.write_all(b"\n")?; let mut item_buf = Vec::new(); // write each item: @@ -466,11 +547,11 @@ impl Envelope { /// Creates a new Envelope from slice. pub fn from_slice(slice: &[u8]) -> Result { - let (header, offset) = Self::parse_header(slice)?; + let (headers, offset) = Self::parse_headers(slice)?; let items = Self::parse_items(slice, offset)?; let mut envelope = Envelope { - event_id: header.event_id, + headers, ..Default::default() }; @@ -484,8 +565,8 @@ impl Envelope { /// Creates a new raw Envelope from the given buffer. pub fn from_bytes_raw(bytes: Vec) -> Result { Ok(Self { - event_id: None, items: Items::Raw(bytes), + ..Default::default() }) } @@ -504,19 +585,16 @@ impl Envelope { Self::from_bytes_raw(bytes) } - fn parse_header(slice: &[u8]) -> Result<(EnvelopeHeader, usize), EnvelopeError> { - let mut stream = serde_json::Deserializer::from_slice(slice).into_iter(); - - let header: EnvelopeHeader = match stream.next() { - None => return Err(EnvelopeError::MissingHeader), - Some(Err(error)) => return Err(EnvelopeError::InvalidHeader(error)), - Some(Ok(header)) => header, - }; + fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> { + let first_line = slice + .splitn(2, |b| *b == b'\n') + .next() + .ok_or(EnvelopeError::MissingNewline)?; - // Each header is terminated by a UNIX newline. - Self::require_termination(slice, stream.byte_offset())?; + let headers: EnvelopeHeaders = + serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?; - Ok((header, stream.byte_offset() + 1)) + Ok((headers, first_line.len() + 1)) } fn parse_items(slice: &[u8], mut offset: usize) -> Result, EnvelopeError> { @@ -848,7 +926,7 @@ some content let envelope = Envelope::from_slice(bytes).unwrap(); let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap(); - assert_eq!(envelope.event_id, Some(event_id)); + assert_eq!(envelope.headers.event_id, Some(event_id)); assert_eq!(envelope.items().count(), 0); } @@ -1011,6 +1089,20 @@ some content } } + #[test] + fn test_all_envelope_headers_roundtrip() { + let bytes = br#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","sdk":{"name":"3e934135-3f2b-49bc-8756-9f025b55143e","version":"3e31738e-4106-42d0-8be2-4a3a1bc648d3","integrations":["daec50ae-8729-49b5-82f7-991446745cd5","8fc94968-3499-4a2c-b4d7-ecc058d9c1b0"],"packages":[{"name":"b59a1949-9950-4203-b394-ddd8d02c9633","version":"3d7790f3-7f32-43f7-b82f-9f5bc85205a8"}]},"sent_at":"2020-02-07T14:16:00Z","trace":{"trace_id":"65bcd18546c942069ed957b15b4ace7c","public_key":"5d593cac-f833-4845-bb23-4eabdf720da2","sample_rate":"0.00000021","sampled":"true","sample_rand":"0.00000012","environment":"0666ab02-6364-4135-aa59-02e8128ce052","transaction":"0252ec25-cd0a-4230-bd2f-936a4585637e"}} +{"type":"event","length":74} +{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296} +"#; + + let envelope = Envelope::from_slice(bytes); + assert!(envelope.is_ok()); + let envelope = envelope.unwrap(); + let serialized = to_str(envelope); + assert_eq!(bytes, serialized.as_bytes()); + } + // Test all possible item types in a single envelope #[test] fn test_deserialize_serialized() { diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 6871d7998..ffad7420e 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -22,7 +22,7 @@ use thiserror::Error; pub use url::Url; pub use uuid::Uuid; -use crate::utils::{ts_rfc3339_opt, ts_seconds_float}; +use crate::utils::{display_from_str_opt, ts_rfc3339_opt, ts_seconds_float}; pub use super::attachment::*; pub use super::envelope::*; @@ -2336,3 +2336,67 @@ impl<'de> Deserialize<'de> for LogAttribute { deserializer.deserialize_map(LogAttributeVisitor) } } + +/// An ID that identifies an organization in the Sentry backend. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub struct OrganizationId(u64); + +impl std::str::FromStr for OrganizationId { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + s.parse().map(Self) + } +} + +impl std::fmt::Display for OrganizationId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The [Dynamic Sampling +/// Context](https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/). +/// +/// Sentry supports sampling at the server level through [Dynamic Sampling](https://docs.sentry.io/organization/dynamic-sampling/). +/// This feature allows users to specify target sample rates for each project via the frontend instead of requiring an application redeployment. +/// The backend needs additional information from the SDK to support these features, contained in +/// the Dynamic Sampling Context. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DynamicSamplingContext { + // Strictly required fields + // Still typed as optional, as when deserializing an envelope created by an older SDK they might still be missing + #[serde(default, skip_serializing_if = "Option::is_none")] + trace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + public_key: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + sample_rate: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + sampled: Option, + // Required fields + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + sample_rand: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + environment: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + transaction: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "display_from_str_opt" + )] + org_id: Option, +} diff --git a/sentry-types/src/utils.rs b/sentry-types/src/utils.rs index f5edfb575..4b4bf1564 100644 --- a/sentry-types/src/utils.rs +++ b/sentry-types/src/utils.rs @@ -189,6 +189,38 @@ pub mod ts_rfc3339_opt { } } +// Serialize and deserialize the inner value into/from a string using the `ToString`/`FromStr` implementation. +pub mod display_from_str_opt { + use serde::{de, ser, Deserialize}; + + pub fn serialize(value: &Option, serializer: S) -> Result + where + T: ToString, + S: ser::Serializer, + { + match value { + Some(t) => serializer.serialize_str(&t.to_string()), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: std::str::FromStr, + T::Err: std::fmt::Display, + D: de::Deserializer<'de>, + { + let opt_string = Option::::deserialize(deserializer)?; + + match opt_string { + Some(s) => T::from_str(&s) + .map(Some) + .map_err(|e| de::Error::custom(format!("failed to parse string to type: {e}"))), + None => Ok(None), + } + } +} + #[cfg(test)] mod tests { use super::timestamp_to_datetime; diff --git a/sentry/src/transports/thread.rs b/sentry/src/transports/thread.rs index 7f45990ff..45d6a0219 100644 --- a/sentry/src/transports/thread.rs +++ b/sentry/src/transports/thread.rs @@ -7,6 +7,7 @@ use std::time::Duration; use super::ratelimit::{RateLimiter, RateLimitingCategory}; use crate::{sentry_debug, Envelope}; +#[expect(clippy::large_enum_variant)] enum Task { SendEnvelope(Envelope), Flush(SyncSender<()>), diff --git a/sentry/src/transports/tokio_thread.rs b/sentry/src/transports/tokio_thread.rs index 9323e482c..21cd19043 100644 --- a/sentry/src/transports/tokio_thread.rs +++ b/sentry/src/transports/tokio_thread.rs @@ -7,6 +7,7 @@ use std::time::Duration; use super::ratelimit::{RateLimiter, RateLimitingCategory}; use crate::{sentry_debug, Envelope}; +#[expect(clippy::large_enum_variant)] enum Task { SendEnvelope(Envelope), Flush(SyncSender<()>), From 7d3898c577a288f645fff82e94d6e3c8165a7349 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 22 Jul 2025 15:00:18 +0200 Subject: [PATCH 02/28] no pub for now --- sentry-types/src/protocol/envelope.rs | 69 +-------------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 925686336..02812273b 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -42,7 +42,7 @@ pub enum EnvelopeError { /// The supported [Sentry Envelope Headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers). #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct EnvelopeHeaders { +struct EnvelopeHeaders { #[serde(default, skip_serializing_if = "Option::is_none")] event_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -59,63 +59,6 @@ pub struct EnvelopeHeaders { trace: Option, } -impl EnvelopeHeaders { - /// Returns the Envelope's Event ID, matching the Event ID of the contained event/transaction, - /// if any. - pub fn event_id(&self) -> Option<&Uuid> { - self.event_id.as_ref() - } - - /// Sets the Event ID. - pub fn set_event_id(&mut self, event_id: Option) { - self.event_id = event_id; - } - - /// Returns the DSN. - pub fn dsn(&self) -> Option<&Dsn> { - self.dsn.as_ref() - } - - /// Sets the DSN. - pub fn set_dsn(&mut self, dsn: Option) { - self.dsn = dsn; - } - - /// Returns the SDK information. - pub fn sdk(&self) -> Option<&ClientSdkInfo> { - self.sdk.as_ref() - } - - /// Sets the SDK information. - pub fn set_sdk(&mut self, sdk: Option) { - self.sdk = sdk; - } - - /// Returns the time this envelope was sent. - pub fn sent_at(&self) -> Option<&SystemTime> { - self.sent_at.as_ref() - } - - /// Sets the time this envelope was sent. - /// The value of this timestamp should be generated as close as possible to the transmission of - /// the event. - /// If offline caching is implemented, the SDK should avoid writing this value when envelopes - /// are saved to disk. - pub fn set_sent_at(&mut self, sent_at: Option) { - self.sent_at = sent_at; - } - - /// Returns the Dynamic Sampling Context. - pub fn trace(&self) -> Option<&DynamicSamplingContext> { - self.trace.as_ref() - } - - /// Sets the Dynamic Sampling Context. - pub fn set_trace(&mut self, trace: Option) { - self.trace = trace; - } -} - /// An Envelope Item Type. #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] #[non_exhaustive] @@ -391,16 +334,6 @@ impl Envelope { EnvelopeItemIter { inner } } - /// Returns a reference to the Envelope's headers. - pub fn headers(&self) -> &EnvelopeHeaders { - &self.headers - } - - /// Sets the Envelope's headers. - pub fn set_headers(&mut self, headers: EnvelopeHeaders) { - self.headers = headers; - } - /// Returns the Envelopes Uuid, if any. pub fn uuid(&self) -> Option<&Uuid> { self.headers.event_id.as_ref() From 740739ddf2c2171537b501dddf337bf09b99fce5 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 22 Jul 2025 15:07:02 +0200 Subject: [PATCH 03/28] improve --- sentry-types/src/protocol/envelope.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 02812273b..530787c06 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -7,11 +7,11 @@ use uuid::Uuid; use crate::utils::ts_rfc3339_opt; use crate::Dsn; -use super::v7::{self as protocol, ClientSdkInfo, DynamicSamplingContext}; +use super::v7::{self as protocol}; use protocol::{ - Attachment, AttachmentType, Event, Log, MonitorCheckIn, SessionAggregates, SessionUpdate, - Transaction, + Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn, + SessionAggregates, SessionUpdate, Transaction, }; /// Raised if a envelope cannot be parsed from a given input. From dae4b5728669751f3fc0caa62ad8f55e0b9502c0 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 22 Jul 2025 15:17:33 +0200 Subject: [PATCH 04/28] improve --- sentry-types/src/protocol/v7.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index ffad7420e..fd34bda19 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2339,7 +2339,7 @@ impl<'de> Deserialize<'de> for LogAttribute { /// An ID that identifies an organization in the Sentry backend. #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] -pub struct OrganizationId(u64); +struct OrganizationId(u64); impl std::str::FromStr for OrganizationId { type Err = std::num::ParseIntError; @@ -2363,7 +2363,7 @@ impl std::fmt::Display for OrganizationId { /// The backend needs additional information from the SDK to support these features, contained in /// the Dynamic Sampling Context. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct DynamicSamplingContext { +pub(crate) struct DynamicSamplingContext { // Strictly required fields // Still typed as optional, as when deserializing an envelope created by an older SDK they might still be missing #[serde(default, skip_serializing_if = "Option::is_none")] From 5ea53606ade0686fb07f964aa0971460d6bd5802 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 22 Jul 2025 15:22:08 +0200 Subject: [PATCH 05/28] improve --- sentry-types/src/protocol/envelope.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 530787c06..04e9af6ba 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -409,7 +409,7 @@ impl Envelope { // write the headers: serde_json::to_writer(&mut writer, &self.headers)?; - writer.write_all(b"\n")?; + writeln!(writer)?; let mut item_buf = Vec::new(); // write each item: From 9f24e6efbeaca1e0e1768ecf5748bd7ac8186ac6 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 23 Jul 2025 14:55:00 +0200 Subject: [PATCH 06/28] improve --- sentry-types/src/protocol/envelope.rs | 18 +++++++++---- sentry-types/src/protocol/v7.rs | 38 ++++++++++++++++++++++++--- sentry-types/src/utils.rs | 2 +- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 04e9af6ba..fd599680a 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1,4 +1,8 @@ -use std::{io::Write, path::Path, time::SystemTime}; +use std::{ + io::{BufRead, Write}, + path::Path, + time::SystemTime, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -519,13 +523,17 @@ impl Envelope { } fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> { - let first_line = slice - .splitn(2, |b| *b == b'\n') + let mut lines = slice.lines(); + let first_line = lines .next() - .ok_or(EnvelopeError::MissingNewline)?; + .ok_or(EnvelopeError::MissingHeader)? + .map_err(|_| EnvelopeError::MissingHeader)?; + if lines.next().is_none() { + return Err(EnvelopeError::MissingNewline); + } let headers: EnvelopeHeaders = - serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?; + serde_json::from_str(first_line.as_str()).map_err(EnvelopeError::InvalidHeader)?; Ok((headers, first_line.len() + 1)) } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index fd34bda19..945727bf6 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2341,6 +2341,12 @@ impl<'de> Deserialize<'de> for LogAttribute { #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] struct OrganizationId(u64); +impl From for OrganizationId { + fn from(value: u64) -> Self { + Self(value) + } +} + impl std::str::FromStr for OrganizationId { type Err = std::num::ParseIntError; @@ -2355,6 +2361,30 @@ impl std::fmt::Display for OrganizationId { } } +/// A random number generated at the start of a trace by the head of trace SDK. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +struct SampleRand(f64); + +impl From for SampleRand { + fn from(value: f64) -> Self { + Self(value) + } +} + +impl std::str::FromStr for SampleRand { + type Err = std::num::ParseFloatError; + + fn from_str(s: &str) -> Result { + s.parse().map(Self) + } +} + +impl std::fmt::Display for SampleRand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.6}", self.0) + } +} + /// The [Dynamic Sampling /// Context](https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/). /// @@ -2376,19 +2406,21 @@ pub(crate) struct DynamicSamplingContext { with = "display_from_str_opt" )] sample_rate: Option, + // Required fields #[serde( default, skip_serializing_if = "Option::is_none", with = "display_from_str_opt" )] - sampled: Option, - // Required fields + sample_rand: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "display_from_str_opt" )] - sample_rand: Option, + sampled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + release: Option, #[serde(default, skip_serializing_if = "Option::is_none")] environment: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/sentry-types/src/utils.rs b/sentry-types/src/utils.rs index 4b4bf1564..c8701e67d 100644 --- a/sentry-types/src/utils.rs +++ b/sentry-types/src/utils.rs @@ -190,7 +190,7 @@ pub mod ts_rfc3339_opt { } // Serialize and deserialize the inner value into/from a string using the `ToString`/`FromStr` implementation. -pub mod display_from_str_opt { +pub(crate) mod display_from_str_opt { use serde::{de, ser, Deserialize}; pub fn serialize(value: &Option, serializer: S) -> Result From ccd1de8db1192ce29bfeebfdfb2e22a042210329 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 23 Jul 2025 15:24:42 +0200 Subject: [PATCH 07/28] improve --- sentry-types/src/protocol/envelope.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index fd599680a..b0dbd17fa 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -35,7 +35,7 @@ pub enum EnvelopeError { MissingNewline, /// Invalid envelope header #[error("invalid envelope header")] - InvalidHeader(#[source] serde_json::Error), + InvalidHeader(#[source] Box), /// Invalid item header #[error("invalid item header")] InvalidItemHeader(#[source] serde_json::Error), @@ -527,13 +527,10 @@ impl Envelope { let first_line = lines .next() .ok_or(EnvelopeError::MissingHeader)? - .map_err(|_| EnvelopeError::MissingHeader)?; - if lines.next().is_none() { - return Err(EnvelopeError::MissingNewline); - } + .map_err(|err| EnvelopeError::InvalidHeader(Box::new(err)))?; - let headers: EnvelopeHeaders = - serde_json::from_str(first_line.as_str()).map_err(EnvelopeError::InvalidHeader)?; + let headers: EnvelopeHeaders = serde_json::from_str(first_line.as_str()) + .map_err(|err| EnvelopeError::InvalidHeader(Box::new(err)))?; Ok((headers, first_line.len() + 1)) } @@ -1032,7 +1029,7 @@ some content #[test] fn test_all_envelope_headers_roundtrip() { - let bytes = br#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","sdk":{"name":"3e934135-3f2b-49bc-8756-9f025b55143e","version":"3e31738e-4106-42d0-8be2-4a3a1bc648d3","integrations":["daec50ae-8729-49b5-82f7-991446745cd5","8fc94968-3499-4a2c-b4d7-ecc058d9c1b0"],"packages":[{"name":"b59a1949-9950-4203-b394-ddd8d02c9633","version":"3d7790f3-7f32-43f7-b82f-9f5bc85205a8"}]},"sent_at":"2020-02-07T14:16:00Z","trace":{"trace_id":"65bcd18546c942069ed957b15b4ace7c","public_key":"5d593cac-f833-4845-bb23-4eabdf720da2","sample_rate":"0.00000021","sampled":"true","sample_rand":"0.00000012","environment":"0666ab02-6364-4135-aa59-02e8128ce052","transaction":"0252ec25-cd0a-4230-bd2f-936a4585637e"}} + let bytes = br#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","sdk":{"name":"3e934135-3f2b-49bc-8756-9f025b55143e","version":"3e31738e-4106-42d0-8be2-4a3a1bc648d3","integrations":["daec50ae-8729-49b5-82f7-991446745cd5","8fc94968-3499-4a2c-b4d7-ecc058d9c1b0"],"packages":[{"name":"b59a1949-9950-4203-b394-ddd8d02c9633","version":"3d7790f3-7f32-43f7-b82f-9f5bc85205a8"}]},"sent_at":"2020-02-07T14:16:00Z","trace":{"trace_id":"65bcd18546c942069ed957b15b4ace7c","public_key":"5d593cac-f833-4845-bb23-4eabdf720da2","sample_rate":"0.00000021","sample_rand":"0.123456","sampled":"true","environment":"0666ab02-6364-4135-aa59-02e8128ce052","transaction":"0252ec25-cd0a-4230-bd2f-936a4585637e"}} {"type":"event","length":74} {"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296} "#; From 17d75e77ee2cd0d06f22c3fdf98f956a092b3c6a Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 10:55:56 +0200 Subject: [PATCH 08/28] improve --- sentry-types/src/protocol/envelope.rs | 24 ++++++++++------------ sentry-types/src/utils.rs | 29 ++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index b0dbd17fa..4cc1d0cc2 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1,8 +1,4 @@ -use std::{ - io::{BufRead, Write}, - path::Path, - time::SystemTime, -}; +use std::{io::Write, path::Path, time::SystemTime}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -35,7 +31,7 @@ pub enum EnvelopeError { MissingNewline, /// Invalid envelope header #[error("invalid envelope header")] - InvalidHeader(#[source] Box), + InvalidHeader(#[source] serde_json::Error), /// Invalid item header #[error("invalid item header")] InvalidItemHeader(#[source] serde_json::Error), @@ -523,16 +519,18 @@ impl Envelope { } fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> { - let mut lines = slice.lines(); - let first_line = lines + let first_line = slice + .split(|b| *b == b'\n') .next() - .ok_or(EnvelopeError::MissingHeader)? - .map_err(|err| EnvelopeError::InvalidHeader(Box::new(err)))?; + .ok_or(EnvelopeError::MissingHeader)?; + + let headers: EnvelopeHeaders = + serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?; - let headers: EnvelopeHeaders = serde_json::from_str(first_line.as_str()) - .map_err(|err| EnvelopeError::InvalidHeader(Box::new(err)))?; + let offset = first_line.len(); + Self::require_termination(slice, offset)?; - Ok((headers, first_line.len() + 1)) + Ok((headers, offset + 1)) } fn parse_items(slice: &[u8], mut offset: usize) -> Result, EnvelopeError> { diff --git a/sentry-types/src/utils.rs b/sentry-types/src/utils.rs index c8701e67d..7f7d7bd26 100644 --- a/sentry-types/src/utils.rs +++ b/sentry-types/src/utils.rs @@ -189,7 +189,34 @@ pub mod ts_rfc3339_opt { } } -// Serialize and deserialize the inner value into/from a string using the `ToString`/`FromStr` implementation. +/// Serialize and deserialize the inner value into/from a string using the `ToString`/`FromStr` implementation. +/// +/// # Example +/// +/// ```rust +/// use serde::{Deserialize, Serialize}; +/// +/// #[derive(Debug, PartialEq, Serialize, Deserialize)] +/// struct Config { +/// #[serde(with = "sentry_types::utils::display_from_str_opt")] +/// host: Option, +/// #[serde(with = "sentry_types::utils::display_from_str_opt")] +/// port: Option, +/// #[serde(with = "sentry_types::utils::display_from_str_opt")] +/// enabled: Option, +/// } +/// +/// let config = Config { +/// host: Some("localhost".to_string()), +/// port: Some(8080), +/// enabled: Some(true), +/// }; +/// let json = serde_json::to_string(&config).unwrap(); +/// assert_eq!(json, r#"{"host":"localhost","port":"8080","enabled":"true"}"#); +/// +/// let deserialized: Config = serde_json::from_str(&json).unwrap(); +/// assert_eq!(deserialized, config); +/// ``` pub(crate) mod display_from_str_opt { use serde::{de, ser, Deserialize}; From 5480d79aed4123451fc78d7cd7295b89c0ba3dcb Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 10:56:33 +0200 Subject: [PATCH 09/28] improve --- sentry-types/src/protocol/envelope.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 4cc1d0cc2..9200fa090 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::utils::ts_rfc3339_opt; use crate::Dsn; -use super::v7::{self as protocol}; +use super::v7 as protocol; use protocol::{ Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn, From 5cdb44cb79f8ac7ff95cfe8bae41cd439555b561 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 11:06:23 +0200 Subject: [PATCH 10/28] improve --- sentry-types/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-types/src/utils.rs b/sentry-types/src/utils.rs index 7f7d7bd26..83d7a6581 100644 --- a/sentry-types/src/utils.rs +++ b/sentry-types/src/utils.rs @@ -193,7 +193,7 @@ pub mod ts_rfc3339_opt { /// /// # Example /// -/// ```rust +/// ```ignore /// use serde::{Deserialize, Serialize}; /// /// #[derive(Debug, PartialEq, Serialize, Deserialize)] From 280abd88b0c11c8cc697c4fdbbd4d6e53f110458 Mon Sep 17 00:00:00 2001 From: lcian Date: Wed, 23 Jul 2025 18:11:51 +0200 Subject: [PATCH 11/28] feat(types): add accessors for envelope headers --- sentry-types/src/protocol/envelope.rs | 70 ++++++++++++++++- sentry-types/src/protocol/v7.rs | 106 +++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 5 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 9200fa090..2fdb80cee 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -42,7 +42,7 @@ pub enum EnvelopeError { /// The supported [Sentry Envelope Headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers). #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -struct EnvelopeHeaders { +pub struct EnvelopeHeaders { #[serde(default, skip_serializing_if = "Option::is_none")] event_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -59,6 +59,64 @@ struct EnvelopeHeaders { trace: Option, } +impl EnvelopeHeaders { + /// Creates empty Envelope headers. + pub fn new() -> EnvelopeHeaders { + Default::default() + } + + /// Returns the Event ID. + pub fn event_id(&self) -> Option<&Uuid> { + self.event_id.as_ref() + } + + /// Sets the Event ID. + pub fn set_event_id(&mut self, event_id: Option) { + self.event_id = event_id; + } + + /// Returns the DSN. + pub fn dsn(&self) -> Option<&Dsn> { + self.dsn.as_ref() + } + + /// Sets the DSN. + pub fn set_dsn(&mut self, dsn: Option) { + self.dsn = dsn; + } + + /// Returns the SDK information. + pub fn sdk(&self) -> Option<&ClientSdkInfo> { + self.sdk.as_ref() + } + + /// Sets the SDK information. + pub fn set_sdk(&mut self, sdk: Option) { + self.sdk = sdk; + } + + /// Returns the time this envelope was sent at. + pub fn sent_at(&self) -> Option<&SystemTime> { + self.sent_at.as_ref() + } + + /// Sets the time this envelope was sent at. + /// This timestamp should be generated as close as possible to the transmision of the event. + pub fn set_sent_at(&mut self, sent_at: Option) { + self.sent_at = sent_at; + } + + /// Returns the Dynamic Sampling Context. + pub fn trace(&self) -> Option<&DynamicSamplingContext> { + self.trace.as_ref() + } + + /// Sets the Dynamic Sampling Context. + pub fn set_trace(&mut self, trace: Option) { + self.trace = trace; + } +} + /// An Envelope Item Type. #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] #[non_exhaustive] @@ -334,6 +392,16 @@ impl Envelope { EnvelopeItemIter { inner } } + /// Returns the Envelope headers. + pub fn headers(&self) -> &EnvelopeHeaders { + &self.headers + } + + /// Sets the Envelope headers. + pub fn set_headers(&mut self, headers: EnvelopeHeaders) { + self.headers = headers + } + /// Returns the Envelopes Uuid, if any. pub fn uuid(&self) -> Option<&Uuid> { self.headers.event_id.as_ref() diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 945727bf6..f799cf23a 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2339,7 +2339,7 @@ impl<'de> Deserialize<'de> for LogAttribute { /// An ID that identifies an organization in the Sentry backend. #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] -struct OrganizationId(u64); +pub struct OrganizationId(u64); impl From for OrganizationId { fn from(value: u64) -> Self { @@ -2363,7 +2363,7 @@ impl std::fmt::Display for OrganizationId { /// A random number generated at the start of a trace by the head of trace SDK. #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] -struct SampleRand(f64); +pub struct SampleRand(f64); impl From for SampleRand { fn from(value: f64) -> Self { @@ -2392,8 +2392,8 @@ impl std::fmt::Display for SampleRand { /// This feature allows users to specify target sample rates for each project via the frontend instead of requiring an application redeployment. /// The backend needs additional information from the SDK to support these features, contained in /// the Dynamic Sampling Context. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub(crate) struct DynamicSamplingContext { +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct DynamicSamplingContext { // Strictly required fields // Still typed as optional, as when deserializing an envelope created by an older SDK they might still be missing #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2432,3 +2432,101 @@ pub(crate) struct DynamicSamplingContext { )] org_id: Option, } + +impl DynamicSamplingContext { + /// Creates an empty Dynamic Sampling Context. + pub fn new() -> Self { + Default::default() + } + + /// Gets the trace ID. + pub fn trace_id(&self) -> Option<&TraceId> { + self.trace_id.as_ref() + } + + /// Sets the trace ID. + pub fn set_trace_id(&mut self, trace_id: Option) { + self.trace_id = trace_id; + } + + /// Gets the DSN public key. + pub fn public_key(&self) -> Option<&String> { + self.public_key.as_ref() + } + + /// Sets the DSN public key. + pub fn set_public_key(&mut self, public_key: Option) { + self.public_key = public_key; + } + + /// Gets the sample rate. + pub fn sample_rate(&self) -> Option<&f32> { + self.sample_rate.as_ref() + } + + /// Sets the sample rate. + pub fn set_sample_rate(&mut self, sample_rate: Option) { + self.sample_rate = sample_rate; + } + + /// Gets the sample random value generated by the head of trace SDK. + pub fn sample_rand(&self) -> Option<&SampleRand> { + self.sample_rand.as_ref() + } + + /// Sets the sample random value generated by the head of trace SDK. + pub fn set_sample_rand(&mut self, sample_rand: Option) { + self.sample_rand = sample_rand; + } + + /// Gets the sampled flag, true if and only if the trace was sampled. This is set by the head + /// of trace SDK. + pub fn sampled(&self) -> Option<&bool> { + self.sampled.as_ref() + } + + /// Sets the sampled flag. + pub fn set_sampled(&mut self, sampled: Option) { + self.sampled = sampled; + } + + /// Gets the release. + pub fn release(&self) -> Option<&String> { + self.release.as_ref() + } + + /// Sets the release. + pub fn set_release(&mut self, release: Option) { + self.release = release; + } + + /// Gets the environment. + pub fn environment(&self) -> Option<&String> { + self.environment.as_ref() + } + + /// Sets the environment. + pub fn set_environment(&mut self, environment: Option) { + self.environment = environment; + } + + /// Gets the transaction. + pub fn transaction(&self) -> Option<&String> { + self.transaction.as_ref() + } + + /// Sets the transaction. + pub fn set_transaction(&mut self, transaction: Option) { + self.transaction = transaction; + } + + /// Gets the organization ID. + pub fn org_id(&self) -> Option<&OrganizationId> { + self.org_id.as_ref() + } + + /// Sets the organization ID. + pub fn set_org_id(&mut self, org_id: Option) { + self.org_id = org_id; + } +} From 2c94b08795a935f512907019de260fd5aa3d1670 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 11:15:33 +0200 Subject: [PATCH 12/28] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c580e1d..f13569dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - feat(log): support kv feature of log (#851) by @lcian - Attributes added to a `log` record using the `kv` feature are now recorded as attributes on the log sent to Sentry. +- feat(types): add all the missing supported envelope headers ([#867](https://github.com/getsentry/sentry-rust/pull/867)) by @lcian +- feat(types): add getters and setters for envelope headers ([#868](https://github.com/getsentry/sentry-rust/pull/868)) by @lcian + - It's now possible to set all of the [envelope headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers) supported by the protocol when constructing envelopes. ### Fixes From 8ec6826ad0ff3180eedb354f84af3c4b329ade6f Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 13:14:21 +0200 Subject: [PATCH 13/28] improve --- CHANGELOG.md | 2 +- sentry-types/src/protocol/envelope.rs | 45 +++---------- sentry-types/src/protocol/v7.rs | 94 +++++++++------------------ 3 files changed, 41 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f13569dc6..ead2827f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - feat(log): support kv feature of log (#851) by @lcian - Attributes added to a `log` record using the `kv` feature are now recorded as attributes on the log sent to Sentry. - feat(types): add all the missing supported envelope headers ([#867](https://github.com/getsentry/sentry-rust/pull/867)) by @lcian -- feat(types): add getters and setters for envelope headers ([#868](https://github.com/getsentry/sentry-rust/pull/868)) by @lcian +- feat(types): add setters for envelope headers ([#868](https://github.com/getsentry/sentry-rust/pull/868)) by @lcian - It's now possible to set all of the [envelope headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers) supported by the protocol when constructing envelopes. ### Fixes diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 2fdb80cee..112a654f8 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -65,55 +65,30 @@ impl EnvelopeHeaders { Default::default() } - /// Returns the Event ID. - pub fn event_id(&self) -> Option<&Uuid> { - self.event_id.as_ref() - } - /// Sets the Event ID. - pub fn set_event_id(&mut self, event_id: Option) { - self.event_id = event_id; - } - - /// Returns the DSN. - pub fn dsn(&self) -> Option<&Dsn> { - self.dsn.as_ref() + pub fn with_event_id(self, event_id: Option) -> Self { + Self { event_id, ..self } } /// Sets the DSN. - pub fn set_dsn(&mut self, dsn: Option) { - self.dsn = dsn; - } - - /// Returns the SDK information. - pub fn sdk(&self) -> Option<&ClientSdkInfo> { - self.sdk.as_ref() + pub fn with_dsn(self, dsn: Option) -> Self { + Self { dsn, ..self } } /// Sets the SDK information. - pub fn set_sdk(&mut self, sdk: Option) { - self.sdk = sdk; - } - - /// Returns the time this envelope was sent at. - pub fn sent_at(&self) -> Option<&SystemTime> { - self.sent_at.as_ref() + pub fn with_sdk(self, sdk: Option) -> Self { + Self { sdk, ..self } } /// Sets the time this envelope was sent at. /// This timestamp should be generated as close as possible to the transmision of the event. - pub fn set_sent_at(&mut self, sent_at: Option) { - self.sent_at = sent_at; - } - - /// Returns the Dynamic Sampling Context. - pub fn trace(&self) -> Option<&DynamicSamplingContext> { - self.trace.as_ref() + pub fn with_sent_at(self, sent_at: Option) -> Self { + Self { sent_at, ..self } } /// Sets the Dynamic Sampling Context. - pub fn set_trace(&mut self, trace: Option) { - self.trace = trace; + pub fn with_trace(self, trace: Option) -> Self { + Self { trace, ..self } } } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index f799cf23a..841cad274 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2439,94 +2439,60 @@ impl DynamicSamplingContext { Default::default() } - /// Gets the trace ID. - pub fn trace_id(&self) -> Option<&TraceId> { - self.trace_id.as_ref() - } - /// Sets the trace ID. - pub fn set_trace_id(&mut self, trace_id: Option) { - self.trace_id = trace_id; - } - - /// Gets the DSN public key. - pub fn public_key(&self) -> Option<&String> { - self.public_key.as_ref() + pub fn with_trace_id(self, trace_id: Option) -> Self { + Self { trace_id, ..self } } /// Sets the DSN public key. - pub fn set_public_key(&mut self, public_key: Option) { - self.public_key = public_key; - } - - /// Gets the sample rate. - pub fn sample_rate(&self) -> Option<&f32> { - self.sample_rate.as_ref() + pub fn with_public_key(self, public_key: Option) -> Self { + Self { public_key, ..self } } /// Sets the sample rate. - pub fn set_sample_rate(&mut self, sample_rate: Option) { - self.sample_rate = sample_rate; - } - - /// Gets the sample random value generated by the head of trace SDK. - pub fn sample_rand(&self) -> Option<&SampleRand> { - self.sample_rand.as_ref() + pub fn with_sample_rate(self, sample_rate: Option) -> Self { + Self { + sample_rate, + ..self + } } /// Sets the sample random value generated by the head of trace SDK. - pub fn set_sample_rand(&mut self, sample_rand: Option) { - self.sample_rand = sample_rand; - } - - /// Gets the sampled flag, true if and only if the trace was sampled. This is set by the head - /// of trace SDK. - pub fn sampled(&self) -> Option<&bool> { - self.sampled.as_ref() + pub fn with_sample_rand(self, sample_rand: Option) -> Self { + Self { + sample_rand, + ..self + } } /// Sets the sampled flag. - pub fn set_sampled(&mut self, sampled: Option) { - self.sampled = sampled; - } - - /// Gets the release. - pub fn release(&self) -> Option<&String> { - self.release.as_ref() + pub fn with_sampled(self, sampled: Option) -> Self { + Self { sampled, ..self } } /// Sets the release. - pub fn set_release(&mut self, release: Option) { - self.release = release; - } - - /// Gets the environment. - pub fn environment(&self) -> Option<&String> { - self.environment.as_ref() + pub fn with_release(self, release: Option) -> Self { + Self { release, ..self } } /// Sets the environment. - pub fn set_environment(&mut self, environment: Option) { - self.environment = environment; - } - - /// Gets the transaction. - pub fn transaction(&self) -> Option<&String> { - self.transaction.as_ref() + pub fn with_environment(self, environment: Option) -> Self { + Self { + environment, + ..self + } } /// Sets the transaction. - pub fn set_transaction(&mut self, transaction: Option) { - self.transaction = transaction; - } - - /// Gets the organization ID. - pub fn org_id(&self) -> Option<&OrganizationId> { - self.org_id.as_ref() + pub fn with_transaction(self, transaction: Option) -> Self { + Self { + transaction, + ..self + } } /// Sets the organization ID. - pub fn set_org_id(&mut self, org_id: Option) { - self.org_id = org_id; + pub fn with_org_id(self, org_id: Option) -> Self { + Self { org_id, ..self } } } From c8cd1f7e5c5a7d41301aa1bc8cb59d564fdbbe4b Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 13:17:39 +0200 Subject: [PATCH 14/28] improve --- sentry-types/src/protocol/envelope.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 112a654f8..b5308b7a8 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1159,7 +1159,6 @@ some content .into(); let mut envelope: Envelope = Envelope::new(); - envelope.add_item(event); envelope.add_item(transaction); envelope.add_item(session); From 135dff0e4a92451d36818d598b1a0f23bc45c598 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 25 Jul 2025 09:23:17 +0200 Subject: [PATCH 15/28] &mut self --- sentry-types/src/protocol/v7.rs | 57 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 841cad274..a541cdf7d 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2440,59 +2440,56 @@ impl DynamicSamplingContext { } /// Sets the trace ID. - pub fn with_trace_id(self, trace_id: Option) -> Self { - Self { trace_id, ..self } + pub fn with_trace_id(&mut self, trace_id: Option) -> &mut Self { + self.trace_id = trace_id; + self } /// Sets the DSN public key. - pub fn with_public_key(self, public_key: Option) -> Self { - Self { public_key, ..self } + pub fn with_public_key(&mut self, public_key: Option) -> &mut Self { + self.public_key = public_key; + self } /// Sets the sample rate. - pub fn with_sample_rate(self, sample_rate: Option) -> Self { - Self { - sample_rate, - ..self - } + pub fn with_sample_rate(&mut self, sample_rate: Option) -> &mut Self { + self.sample_rate = sample_rate; + self } /// Sets the sample random value generated by the head of trace SDK. - pub fn with_sample_rand(self, sample_rand: Option) -> Self { - Self { - sample_rand, - ..self - } + pub fn with_sample_rand(&mut self, sample_rand: Option) -> &mut Self { + self.sample_rand = sample_rand; + self } /// Sets the sampled flag. - pub fn with_sampled(self, sampled: Option) -> Self { - Self { sampled, ..self } + pub fn with_sampled(&mut self, sampled: Option) -> &mut Self { + self.sampled = sampled; + self } /// Sets the release. - pub fn with_release(self, release: Option) -> Self { - Self { release, ..self } + pub fn with_release(&mut self, release: Option) -> &mut Self { + self.release = release; + self } /// Sets the environment. - pub fn with_environment(self, environment: Option) -> Self { - Self { - environment, - ..self - } + pub fn with_environment(&mut self, environment: Option) -> &mut Self { + self.environment = environment; + self } /// Sets the transaction. - pub fn with_transaction(self, transaction: Option) -> Self { - Self { - transaction, - ..self - } + pub fn with_transaction(&mut self, transaction: Option) -> &mut Self { + self.transaction = transaction; + self } /// Sets the organization ID. - pub fn with_org_id(self, org_id: Option) -> Self { - Self { org_id, ..self } + pub fn with_org_id(&mut self, org_id: Option) -> &mut Self { + self.org_id = org_id; + self } } From e64452e8dd468cea23bddd0fa290743e94354317 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 25 Jul 2025 09:32:27 +0200 Subject: [PATCH 16/28] mut self --- sentry-types/src/protocol/envelope.rs | 32 ++++++++++++++++++--------- sentry-types/src/protocol/v7.rs | 27 ++++++++++++++-------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index b5308b7a8..4a4f93df0 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -66,29 +66,39 @@ impl EnvelopeHeaders { } /// Sets the Event ID. - pub fn with_event_id(self, event_id: Option) -> Self { - Self { event_id, ..self } + #[must_use] + pub fn with_event_id(mut self, event_id: Option) -> Self { + self.event_id = event_id; + self } /// Sets the DSN. - pub fn with_dsn(self, dsn: Option) -> Self { - Self { dsn, ..self } + #[must_use] + pub fn with_dsn(mut self, dsn: Option) -> Self { + self.dsn = dsn; + self } /// Sets the SDK information. - pub fn with_sdk(self, sdk: Option) -> Self { - Self { sdk, ..self } + #[must_use] + pub fn with_sdk(mut self, sdk: Option) -> Self { + self.sdk = sdk; + self } /// Sets the time this envelope was sent at. - /// This timestamp should be generated as close as possible to the transmision of the event. - pub fn with_sent_at(self, sent_at: Option) -> Self { - Self { sent_at, ..self } + /// This timestamp should be generated as close as possible to the transmission of the event. + #[must_use] + pub fn with_sent_at(mut self, sent_at: Option) -> Self { + self.sent_at = sent_at; + self } /// Sets the Dynamic Sampling Context. - pub fn with_trace(self, trace: Option) -> Self { - Self { trace, ..self } + #[must_use] + pub fn with_trace(mut self, trace: Option) -> Self { + self.trace = trace; + self } } diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index a541cdf7d..0ffe47edc 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2440,55 +2440,64 @@ impl DynamicSamplingContext { } /// Sets the trace ID. - pub fn with_trace_id(&mut self, trace_id: Option) -> &mut Self { + #[must_use] + pub fn with_trace_id(mut self, trace_id: Option) -> Self { self.trace_id = trace_id; self } /// Sets the DSN public key. - pub fn with_public_key(&mut self, public_key: Option) -> &mut Self { + #[must_use] + pub fn with_public_key(mut self, public_key: Option) -> Self { self.public_key = public_key; self } /// Sets the sample rate. - pub fn with_sample_rate(&mut self, sample_rate: Option) -> &mut Self { + #[must_use] + pub fn with_sample_rate(mut self, sample_rate: Option) -> Self { self.sample_rate = sample_rate; self } /// Sets the sample random value generated by the head of trace SDK. - pub fn with_sample_rand(&mut self, sample_rand: Option) -> &mut Self { + #[must_use] + pub fn with_sample_rand(mut self, sample_rand: Option) -> Self { self.sample_rand = sample_rand; self } /// Sets the sampled flag. - pub fn with_sampled(&mut self, sampled: Option) -> &mut Self { + #[must_use] + pub fn with_sampled(mut self, sampled: Option) -> Self { self.sampled = sampled; self } /// Sets the release. - pub fn with_release(&mut self, release: Option) -> &mut Self { + #[must_use] + pub fn with_release(mut self, release: Option) -> Self { self.release = release; self } /// Sets the environment. - pub fn with_environment(&mut self, environment: Option) -> &mut Self { + #[must_use] + pub fn with_environment(mut self, environment: Option) -> Self { self.environment = environment; self } /// Sets the transaction. - pub fn with_transaction(&mut self, transaction: Option) -> &mut Self { + #[must_use] + pub fn with_transaction(mut self, transaction: Option) -> Self { self.transaction = transaction; self } /// Sets the organization ID. - pub fn with_org_id(&mut self, org_id: Option) -> &mut Self { + #[must_use] + pub fn with_org_id(mut self, org_id: Option) -> Self { self.org_id = org_id; self } From 53c2c3fd2b95ba1ad20093a9456f05b048be6667 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 28 Jul 2025 16:49:59 +0200 Subject: [PATCH 17/28] address all feedback --- sentry-types/src/protocol/envelope.rs | 44 +++++++++++----- sentry-types/src/protocol/v7.rs | 74 ++++++++++++++++++--------- sentry/src/transports/thread.rs | 5 +- sentry/src/transports/tokio_thread.rs | 5 +- 4 files changed, 89 insertions(+), 39 deletions(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 4a4f93df0..3c8bcf488 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -67,37 +67,37 @@ impl EnvelopeHeaders { /// Sets the Event ID. #[must_use] - pub fn with_event_id(mut self, event_id: Option) -> Self { - self.event_id = event_id; + pub fn with_event_id(mut self, event_id: Uuid) -> Self { + self.event_id = Some(event_id); self } /// Sets the DSN. #[must_use] - pub fn with_dsn(mut self, dsn: Option) -> Self { - self.dsn = dsn; + pub fn with_dsn(mut self, dsn: Dsn) -> Self { + self.dsn = Some(dsn); self } /// Sets the SDK information. #[must_use] - pub fn with_sdk(mut self, sdk: Option) -> Self { - self.sdk = sdk; + pub fn with_sdk(mut self, sdk: ClientSdkInfo) -> Self { + self.sdk = Some(sdk); self } /// Sets the time this envelope was sent at. /// This timestamp should be generated as close as possible to the transmission of the event. #[must_use] - pub fn with_sent_at(mut self, sent_at: Option) -> Self { - self.sent_at = sent_at; + pub fn with_sent_at(mut self, sent_at: SystemTime) -> Self { + self.sent_at = Some(sent_at); self } /// Sets the Dynamic Sampling Context. #[must_use] - pub fn with_trace(mut self, trace: Option) -> Self { - self.trace = trace; + pub fn with_trace(mut self, trace: DynamicSamplingContext) -> Self { + self.trace = Some(trace); self } } @@ -383,8 +383,10 @@ impl Envelope { } /// Sets the Envelope headers. - pub fn set_headers(&mut self, headers: EnvelopeHeaders) { - self.headers = headers + #[must_use] + pub fn with_headers(mut self, headers: EnvelopeHeaders) -> Self { + self.headers = headers; + self } /// Returns the Envelopes Uuid, if any. @@ -699,7 +701,7 @@ mod test { use super::*; use crate::protocol::v7::{ - Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes, + Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SampleRand, SessionAttributes, SessionStatus, Span, }; @@ -1092,6 +1094,22 @@ some content assert_eq!(bytes, serialized.as_bytes()); } + #[test] + fn test_sample_rand_rounding() { + let envelope = Envelope::new().with_headers( + EnvelopeHeaders::new().with_trace( + DynamicSamplingContext::new() + .with_sample_rand(SampleRand::try_from(0.9999999).unwrap()), // 7 nines + ), + ); + let expected = br#"{"trace":{"sample_rand":"0.999999"}} +"#; + + let serialized = to_str(envelope); + println!("{}", serialized.clone()); + assert_eq!(expected, serialized.as_bytes()); + } + // Test all possible item types in a single envelope #[test] fn test_deserialize_serialized() { diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 0ffe47edc..ced3bd3e8 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2365,23 +2365,49 @@ impl std::fmt::Display for OrganizationId { #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct SampleRand(f64); -impl From for SampleRand { - fn from(value: f64) -> Self { - Self(value) +/// An error that indicates failure to construct a SampleRand. +#[derive(Debug, Error)] +pub enum InvalidSampleRandError { + /// Indicates that the given value cannot be converted to a f64 succesfully. + #[error("failed to parse f64: {0}")] + InvalidFloat(#[from] std::num::ParseFloatError), + + /// Indicates that the given float is outside of the valid range for a sample rand, that is the + /// half-open interval [0.0, 1.0). + #[error("sample rand value out of admissible interval [0.0, 1.0)")] + OutOfRange, +} + +impl TryFrom for SampleRand { + type Error = InvalidSampleRandError; + + fn try_from(value: f64) -> Result { + if !(0.0..1.0).contains(&value) { + return Err(InvalidSampleRandError::OutOfRange); + } + Ok(Self(value)) } } impl std::str::FromStr for SampleRand { - type Err = std::num::ParseFloatError; + type Err = InvalidSampleRandError; fn from_str(s: &str) -> Result { - s.parse().map(Self) + let x: f64 = s.parse().map_err(InvalidSampleRandError::InvalidFloat)?; + Self::try_from(x) } } impl std::fmt::Display for SampleRand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.6}", self.0) + // Special case: "{:.6}" would round values greater than or equal to 0.9999995 to 1.0, + // as Rust uses [rounding half-to-even](https://doc.rust-lang.org/std/fmt/#precision). + // Round to 0.999999 instead to comply with spec. + if self.0 >= 0.9999995 { + write!(f, "0.999999") + } else { + write!(f, "{:.6}", self.0) + } } } @@ -2441,64 +2467,64 @@ impl DynamicSamplingContext { /// Sets the trace ID. #[must_use] - pub fn with_trace_id(mut self, trace_id: Option) -> Self { - self.trace_id = trace_id; + pub fn with_trace_id(mut self, trace_id: TraceId) -> Self { + self.trace_id = Some(trace_id); self } /// Sets the DSN public key. #[must_use] - pub fn with_public_key(mut self, public_key: Option) -> Self { - self.public_key = public_key; + pub fn with_public_key(mut self, public_key: String) -> Self { + self.public_key = Some(public_key); self } /// Sets the sample rate. #[must_use] - pub fn with_sample_rate(mut self, sample_rate: Option) -> Self { - self.sample_rate = sample_rate; + pub fn with_sample_rate(mut self, sample_rate: f32) -> Self { + self.sample_rate = Some(sample_rate); self } /// Sets the sample random value generated by the head of trace SDK. #[must_use] - pub fn with_sample_rand(mut self, sample_rand: Option) -> Self { - self.sample_rand = sample_rand; + pub fn with_sample_rand(mut self, sample_rand: SampleRand) -> Self { + self.sample_rand = Some(sample_rand); self } /// Sets the sampled flag. #[must_use] - pub fn with_sampled(mut self, sampled: Option) -> Self { - self.sampled = sampled; + pub fn with_sampled(mut self, sampled: bool) -> Self { + self.sampled = Some(sampled); self } /// Sets the release. #[must_use] - pub fn with_release(mut self, release: Option) -> Self { - self.release = release; + pub fn with_release(mut self, release: String) -> Self { + self.release = Some(release); self } /// Sets the environment. #[must_use] - pub fn with_environment(mut self, environment: Option) -> Self { - self.environment = environment; + pub fn with_environment(mut self, environment: String) -> Self { + self.environment = Some(environment); self } /// Sets the transaction. #[must_use] - pub fn with_transaction(mut self, transaction: Option) -> Self { - self.transaction = transaction; + pub fn with_transaction(mut self, transaction: String) -> Self { + self.transaction = Some(transaction); self } /// Sets the organization ID. #[must_use] - pub fn with_org_id(mut self, org_id: Option) -> Self { - self.org_id = org_id; + pub fn with_org_id(mut self, org_id: OrganizationId) -> Self { + self.org_id = Some(org_id); self } } diff --git a/sentry/src/transports/thread.rs b/sentry/src/transports/thread.rs index 45d6a0219..288336157 100644 --- a/sentry/src/transports/thread.rs +++ b/sentry/src/transports/thread.rs @@ -7,7 +7,10 @@ use std::time::Duration; use super::ratelimit::{RateLimiter, RateLimitingCategory}; use crate::{sentry_debug, Envelope}; -#[expect(clippy::large_enum_variant)] +#[expect( + clippy::large_enum_variant, + reason = "In normal usage this is usually SendEnvelope, the other variants are only used when the user manually calls transport.flush() or when the transport is shut down." +)] enum Task { SendEnvelope(Envelope), Flush(SyncSender<()>), diff --git a/sentry/src/transports/tokio_thread.rs b/sentry/src/transports/tokio_thread.rs index 21cd19043..398e12c67 100644 --- a/sentry/src/transports/tokio_thread.rs +++ b/sentry/src/transports/tokio_thread.rs @@ -7,7 +7,10 @@ use std::time::Duration; use super::ratelimit::{RateLimiter, RateLimitingCategory}; use crate::{sentry_debug, Envelope}; -#[expect(clippy::large_enum_variant)] +#[expect( + clippy::large_enum_variant, + reason = "In normal usage this is usually SendEnvelope, the other variants are only used when the user manually calls transport.flush() or when the transport is shut down." +)] enum Task { SendEnvelope(Envelope), Flush(SyncSender<()>), From ddc2811bb978158991ce508b355bc2430ed3057d Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 28 Jul 2025 16:53:38 +0200 Subject: [PATCH 18/28] remove print --- sentry-types/src/protocol/envelope.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-types/src/protocol/envelope.rs b/sentry-types/src/protocol/envelope.rs index 3c8bcf488..d30dde9b4 100644 --- a/sentry-types/src/protocol/envelope.rs +++ b/sentry-types/src/protocol/envelope.rs @@ -1106,7 +1106,6 @@ some content "#; let serialized = to_str(envelope); - println!("{}", serialized.clone()); assert_eq!(expected, serialized.as_bytes()); } From df7126ab1db2175d3eb3085c8fa9a8817b8f8b33 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 28 Jul 2025 17:01:55 +0200 Subject: [PATCH 19/28] fix whitespace --- sentry-types/src/protocol/v7.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 82ebdba39..ced3bd3e8 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -2341,7 +2341,6 @@ impl<'de> Deserialize<'de> for LogAttribute { #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct OrganizationId(u64); - impl From for OrganizationId { fn from(value: u64) -> Self { Self(value) From 23ed1ce454f02d6526d690e312ec5eee037756ee Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 13:39:53 +0200 Subject: [PATCH 20/28] feat(core): add some DSC fields to transaction envelope headers --- sentry-core/src/performance.rs | 70 ++++++++++++++++++++++++++++------ sentry/Cargo.toml | 2 +- sentry/tests/test_basic.rs | 41 +++++++++++++++++++- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 10c98ac1d..5ca6545ab 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -601,19 +601,50 @@ fn transaction_sample_rate( } } +/// Represents a sampling decision made for a certain transaction. +struct SamplingDecision { + /// Whether the transaction should be sampled. + sampled: bool, + /// The sample rate that this sampling decision is based on. + sample_rate: f32, +} + +impl From<&TransactionContext> for SamplingDecision { + fn from(ctx: &TransactionContext) -> Self { + Self { + sample_rate: ctx + .sampled + .map_or(0.0, |sampled| if sampled { 1.0 } else { 0.0 }), + sampled: ctx.sampled.unwrap_or(false), + } + } +} + /// Determine whether the new transaction should be sampled. #[cfg(feature = "client")] impl Client { - fn is_transaction_sampled(&self, ctx: &TransactionContext) -> bool { + fn determine_sampling_decision(&self, ctx: &TransactionContext) -> SamplingDecision { let client_options = self.options(); - self.sample_should_send(transaction_sample_rate( + let sample_rate = transaction_sample_rate( client_options.traces_sampler.as_deref(), ctx, client_options.traces_sample_rate, - )) + ); + let sampled = self.sample_should_send(sample_rate); + SamplingDecision { + sampled, + sample_rate, + } } } +/// Some metadata associated with a transaction. +#[derive(Clone, Debug)] +struct TransactionMetadata { + /// The sample rate used when making the sampling decision for the associated transaction. + sample_rate: f32, +} + /// A running Performance Monitoring Transaction. /// /// The transaction needs to be explicitly finished via [`Transaction::finish`], @@ -622,6 +653,7 @@ impl Client { #[derive(Clone, Debug)] pub struct Transaction { pub(crate) inner: TransactionArc, + metadata: TransactionMetadata, } /// Iterable for a transaction's [data attributes](protocol::TraceContext::data). @@ -660,15 +692,15 @@ impl<'a> TransactionData<'a> { impl Transaction { #[cfg(feature = "client")] fn new(client: Option>, ctx: TransactionContext) -> Self { - let (sampled, transaction) = match client.as_ref() { + let (sampling_decision, transaction) = match client.as_ref() { Some(client) => ( - client.is_transaction_sampled(&ctx), + client.determine_sampling_decision(&ctx), Some(protocol::Transaction { name: Some(ctx.name), ..Default::default() }), ), - None => (ctx.sampled.unwrap_or(false), None), + None => (SamplingDecision::from(&ctx), None), }; let context = protocol::TraceContext { @@ -682,30 +714,39 @@ impl Transaction { Self { inner: Arc::new(Mutex::new(TransactionInner { client, - sampled, + sampled: sampling_decision.sampled, context, transaction, })), + metadata: TransactionMetadata { + sample_rate: sampling_decision.sample_rate, + }, } } #[cfg(not(feature = "client"))] fn new_noop(ctx: TransactionContext) -> Self { + let sampling_decision = SamplingDecision::from(&ctx); let context = protocol::TraceContext { trace_id: ctx.trace_id, parent_span_id: ctx.parent_span_id, op: Some(ctx.op), ..Default::default() }; - let sampled = ctx.sampled.unwrap_or(false); - Self { + let slf = Self { inner: Arc::new(Mutex::new(TransactionInner { - sampled, + sampled: sampling_decision.sampled, context, transaction: None, })), - } + metadata: TransactionMetadata { + sample_rate: sampling_decision.sample_rate, + }, + }; + // use the field on cfg(not(feature = "client")) + let _ = slf.metadata.sample_rate; + slf } /// Set a data attribute to be sent with this Transaction. @@ -820,9 +861,16 @@ impl Transaction { transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone())); transaction.server_name.clone_from(&opts.server_name); + let dsc = protocol::DynamicSamplingContext::new().with_trace_id(Some(inner.context.trace_id)) + .with_sample_rate(Some(self.metadata.sample_rate)) + .with_public_key(opts.dsn.as_ref().map(|dsn| dsn.public_key().to_owned())) + .with_sampled(Some(inner.sampled)); + drop(inner); let mut envelope = protocol::Envelope::new(); + let headers = protocol::EnvelopeHeaders::new().with_trace(Some(dsc)); + envelope.set_headers(headers); envelope.add_item(transaction); client.send_envelope(envelope) diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index 9a43eed10..e8a40ec91 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -98,7 +98,7 @@ sentry-tracing = { path = "../sentry-tracing" } actix-web = { version = "4", default-features = false } anyhow = { version = "1.0.30" } log = { version = "0.4.8", features = ["std"] } -pretty_env_logger = "0.5.0" +pretty_env_logger = "0.5.0" slog = { version = "2.5.2" } tokio = { version = "1.44", features = ["macros"] } tower = { version = "0.5.2", features = ["util"] } diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index 8f5f4d0bc..9c09008a3 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -3,8 +3,10 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use sentry::protocol::{Attachment, Context, EnvelopeItem}; -use sentry::types::Uuid; +use sentry::protocol::{ + Attachment, Context, DynamicSamplingContext, EnvelopeHeaders, EnvelopeItem, +}; +use sentry::types::{Dsn, Uuid}; #[test] fn test_basic_capture_message() { @@ -578,3 +580,38 @@ fn test_basic_capture_log_macro_message_formatted_with_attributes() { _ => panic!("expected item container"), } } + +#[test] +fn test_transaction_envelope_dsc_headers() { + let mut trace_id: Option = None; + let dsn: Option = "http://foo@example.com/42".parse().ok(); + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let transaction_ctx = sentry::TransactionContext::new("name transaction", "op"); + trace_id = Some(transaction_ctx.trace_id()); + let transaction = sentry::start_transaction(transaction_ctx); + sentry::configure_scope(|scope| scope.set_span(Some(transaction.clone().into()))); + transaction.finish(); + }, + sentry::ClientOptions { + dsn: dsn.clone(), + traces_sample_rate: 1.0, + ..Default::default() + }, + ); + + assert!(trace_id.is_some()); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.into_iter().next().unwrap(); + + let expected = EnvelopeHeaders::new() + .with_event_id(envelope.uuid().copied()) + .with_trace(Some( + DynamicSamplingContext::new() + .with_trace_id(trace_id) + .with_public_key(Some(dsn.unwrap().public_key().to_owned())) + .with_sample_rate(Some(1.0)) + .with_sampled(Some(true)), + )); + assert_eq!(envelope.headers(), &expected); +} From 2b129e3a21116736dda77421f3135208081d8e3d Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 13:41:59 +0200 Subject: [PATCH 21/28] improve --- sentry/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index e8a40ec91..9a43eed10 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -98,7 +98,7 @@ sentry-tracing = { path = "../sentry-tracing" } actix-web = { version = "4", default-features = false } anyhow = { version = "1.0.30" } log = { version = "0.4.8", features = ["std"] } -pretty_env_logger = "0.5.0" +pretty_env_logger = "0.5.0" slog = { version = "2.5.2" } tokio = { version = "1.44", features = ["macros"] } tower = { version = "0.5.2", features = ["util"] } From 567b42058ee94419c83206bdffafc9f9f509e9be Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 13:48:17 +0200 Subject: [PATCH 22/28] improve --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead2827f3..2957c51b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - feat(types): add all the missing supported envelope headers ([#867](https://github.com/getsentry/sentry-rust/pull/867)) by @lcian - feat(types): add setters for envelope headers ([#868](https://github.com/getsentry/sentry-rust/pull/868)) by @lcian - It's now possible to set all of the [envelope headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers) supported by the protocol when constructing envelopes. +- feat(core): add some DSC fields to transaction envelope headers ([#869](https://github.com/getsentry/sentry-rust/pull/869)) by @lcian + - The SDK now sends additional envelope headers with transactions. This should solve some extrapolation issues for span metrics. ### Fixes From 8186794c09746e91cc334bc6bd4b12602f864bb2 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 14:48:23 +0200 Subject: [PATCH 23/28] improve --- sentry-core/src/performance.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 5ca6545ab..4fa83962a 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -861,10 +861,11 @@ impl Transaction { transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone())); transaction.server_name.clone_from(&opts.server_name); - let dsc = protocol::DynamicSamplingContext::new().with_trace_id(Some(inner.context.trace_id)) - .with_sample_rate(Some(self.metadata.sample_rate)) - .with_public_key(opts.dsn.as_ref().map(|dsn| dsn.public_key().to_owned())) - .with_sampled(Some(inner.sampled)); + let dsc = protocol::DynamicSamplingContext::new() + .with_trace_id(Some(inner.context.trace_id)) + .with_sample_rate(Some(self.metadata.sample_rate)) + .with_public_key(opts.dsn.as_ref().map(|dsn| dsn.public_key().to_owned())) + .with_sampled(Some(inner.sampled)); drop(inner); From e186167cf57c767c3a718650f7f08fac68f23367 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 15:16:03 +0200 Subject: [PATCH 24/28] improve --- sentry-core/src/performance.rs | 58 +++++++++++----------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 4fa83962a..e85e3f93e 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -601,29 +601,10 @@ fn transaction_sample_rate( } } -/// Represents a sampling decision made for a certain transaction. -struct SamplingDecision { - /// Whether the transaction should be sampled. - sampled: bool, - /// The sample rate that this sampling decision is based on. - sample_rate: f32, -} - -impl From<&TransactionContext> for SamplingDecision { - fn from(ctx: &TransactionContext) -> Self { - Self { - sample_rate: ctx - .sampled - .map_or(0.0, |sampled| if sampled { 1.0 } else { 0.0 }), - sampled: ctx.sampled.unwrap_or(false), - } - } -} - /// Determine whether the new transaction should be sampled. #[cfg(feature = "client")] impl Client { - fn determine_sampling_decision(&self, ctx: &TransactionContext) -> SamplingDecision { + fn determine_sampling_decision(&self, ctx: &TransactionContext) -> (bool, f32) { let client_options = self.options(); let sample_rate = transaction_sample_rate( client_options.traces_sampler.as_deref(), @@ -631,14 +612,12 @@ impl Client { client_options.traces_sample_rate, ); let sampled = self.sample_should_send(sample_rate); - SamplingDecision { - sampled, - sample_rate, - } + (sampled, sample_rate) } } /// Some metadata associated with a transaction. +#[cfg(feature = "client")] #[derive(Clone, Debug)] struct TransactionMetadata { /// The sample rate used when making the sampling decision for the associated transaction. @@ -653,6 +632,7 @@ struct TransactionMetadata { #[derive(Clone, Debug)] pub struct Transaction { pub(crate) inner: TransactionArc, + #[cfg(feature = "client")] metadata: TransactionMetadata, } @@ -692,7 +672,7 @@ impl<'a> TransactionData<'a> { impl Transaction { #[cfg(feature = "client")] fn new(client: Option>, ctx: TransactionContext) -> Self { - let (sampling_decision, transaction) = match client.as_ref() { + let ((sampled, sample_rate), transaction) = match client.as_ref() { Some(client) => ( client.determine_sampling_decision(&ctx), Some(protocol::Transaction { @@ -700,7 +680,14 @@ impl Transaction { ..Default::default() }), ), - None => (SamplingDecision::from(&ctx), None), + None => ( + ( + ctx.sampled.unwrap_or(false), + ctx.sampled + .map_or(0.0, |sampled| if sampled { 1.0 } else { 0.0 }), + ), + None, + ), }; let context = protocol::TraceContext { @@ -714,19 +701,16 @@ impl Transaction { Self { inner: Arc::new(Mutex::new(TransactionInner { client, - sampled: sampling_decision.sampled, + sampled, context, transaction, })), - metadata: TransactionMetadata { - sample_rate: sampling_decision.sample_rate, - }, + metadata: TransactionMetadata { sample_rate }, } } #[cfg(not(feature = "client"))] fn new_noop(ctx: TransactionContext) -> Self { - let sampling_decision = SamplingDecision::from(&ctx); let context = protocol::TraceContext { trace_id: ctx.trace_id, parent_span_id: ctx.parent_span_id, @@ -734,19 +718,13 @@ impl Transaction { ..Default::default() }; - let slf = Self { + Self { inner: Arc::new(Mutex::new(TransactionInner { - sampled: sampling_decision.sampled, + sampled: ctx.sampled.unwrap_or(false), context, transaction: None, })), - metadata: TransactionMetadata { - sample_rate: sampling_decision.sample_rate, - }, - }; - // use the field on cfg(not(feature = "client")) - let _ = slf.metadata.sample_rate; - slf + } } /// Set a data attribute to be sent with this Transaction. From c90f2d777824811aa49be766bb79f5e05e86645f Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 24 Jul 2025 15:22:05 +0200 Subject: [PATCH 25/28] improve --- sentry-core/src/performance.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index e85e3f93e..9fd92492c 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -717,10 +717,11 @@ impl Transaction { op: Some(ctx.op), ..Default::default() }; + let sampled = ctx.sampled.unwrap_or(false); Self { inner: Arc::new(Mutex::new(TransactionInner { - sampled: ctx.sampled.unwrap_or(false), + sampled, context, transaction: None, })), From 28eab2567b65d24a6050e04727fae237f73fc022 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 28 Jul 2025 17:11:46 +0200 Subject: [PATCH 26/28] improve --- sentry-core/src/performance.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 9fd92492c..44380008b 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; use std::time::SystemTime; use sentry_types::protocol::v7::SpanId; +use sentry_types::Dsn; use crate::{protocol, Hub}; @@ -594,10 +595,7 @@ fn transaction_sample_rate( ) -> f32 { match (traces_sampler, traces_sample_rate) { (Some(traces_sampler), _) => traces_sampler(ctx), - (None, traces_sample_rate) => ctx - .sampled - .map(|sampled| if sampled { 1.0 } else { 0.0 }) - .unwrap_or(traces_sample_rate), + (None, traces_sample_rate) => ctx.sampled.map(f32::from).unwrap_or(traces_sample_rate), } } @@ -683,8 +681,7 @@ impl Transaction { None => ( ( ctx.sampled.unwrap_or(false), - ctx.sampled - .map_or(0.0, |sampled| if sampled { 1.0 } else { 0.0 }), + ctx.sampled.map_or(0.0, f32::from), ), None, ), @@ -840,17 +837,19 @@ impl Transaction { transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone())); transaction.server_name.clone_from(&opts.server_name); - let dsc = protocol::DynamicSamplingContext::new() - .with_trace_id(Some(inner.context.trace_id)) - .with_sample_rate(Some(self.metadata.sample_rate)) - .with_public_key(opts.dsn.as_ref().map(|dsn| dsn.public_key().to_owned())) - .with_sampled(Some(inner.sampled)); + let mut dsc = protocol::DynamicSamplingContext::new() + .with_trace_id(inner.context.trace_id) + .with_sample_rate(self.metadata.sample_rate) + .with_sampled(inner.sampled); + if let Some(public_key) = client.dsn().map(Dsn::public_key) { + dsc = dsc.with_public_key(public_key.to_owned()); + } drop(inner); - let mut envelope = protocol::Envelope::new(); - let headers = protocol::EnvelopeHeaders::new().with_trace(Some(dsc)); - envelope.set_headers(headers); + let mut envelope = protocol::Envelope::new().with_headers( + protocol::EnvelopeHeaders::new().with_trace(dsc) + ); envelope.add_item(transaction); client.send_envelope(envelope) From 37d63888ee4ee3366cceb0017de272c99ef698e2 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 28 Jul 2025 17:16:50 +0200 Subject: [PATCH 27/28] improve --- sentry-core/src/performance.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 44380008b..18a96d1f3 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -5,7 +5,6 @@ use std::sync::{Arc, Mutex, MutexGuard}; use std::time::SystemTime; use sentry_types::protocol::v7::SpanId; -use sentry_types::Dsn; use crate::{protocol, Hub}; @@ -841,7 +840,7 @@ impl Transaction { .with_trace_id(inner.context.trace_id) .with_sample_rate(self.metadata.sample_rate) .with_sampled(inner.sampled); - if let Some(public_key) = client.dsn().map(Dsn::public_key) { + if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) { dsc = dsc.with_public_key(public_key.to_owned()); } From 34cbb3f69747b901cfe248f7b230e097626d1013 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 28 Jul 2025 17:19:34 +0200 Subject: [PATCH 28/28] improve --- sentry/tests/test_basic.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index 9c09008a3..21cb98ab3 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -601,17 +601,18 @@ fn test_transaction_envelope_dsc_headers() { ); assert!(trace_id.is_some()); + let trace_id = trace_id.unwrap(); assert_eq!(envelopes.len(), 1); let envelope = envelopes.into_iter().next().unwrap(); - - let expected = EnvelopeHeaders::new() - .with_event_id(envelope.uuid().copied()) - .with_trace(Some( - DynamicSamplingContext::new() - .with_trace_id(trace_id) - .with_public_key(Some(dsn.unwrap().public_key().to_owned())) - .with_sample_rate(Some(1.0)) - .with_sampled(Some(true)), - )); + assert!(envelope.uuid().is_some()); + let uuid = envelope.uuid().copied().unwrap(); + + let expected = EnvelopeHeaders::new().with_event_id(uuid).with_trace( + DynamicSamplingContext::new() + .with_trace_id(trace_id) + .with_public_key(dsn.unwrap().public_key().to_owned()) + .with_sample_rate(1.0) + .with_sampled(true), + ); assert_eq!(envelope.headers(), &expected); }