From b16d9c3e05a59df2aa4f60c012c7e6ec22b70ae8 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 22 Jul 2025 14:57:07 +0200 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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)]