Skip to content

feat(types): add setters for envelope headers #868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Expand Down
75 changes: 72 additions & 3 deletions sentry-types/src/protocol/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand All @@ -59,6 +59,49 @@ struct EnvelopeHeaders {
trace: Option<DynamicSamplingContext>,
}

impl EnvelopeHeaders {
/// Creates empty Envelope headers.
pub fn new() -> EnvelopeHeaders {
Default::default()
}

/// Sets the Event ID.
#[must_use]
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: Dsn) -> Self {
self.dsn = Some(dsn);
self
}

/// Sets the SDK information.
#[must_use]
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: SystemTime) -> Self {
self.sent_at = Some(sent_at);
self
}

/// Sets the Dynamic Sampling Context.
#[must_use]
pub fn with_trace(mut self, trace: DynamicSamplingContext) -> Self {
self.trace = Some(trace);
self
}
}

/// An Envelope Item Type.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
#[non_exhaustive]
Expand Down Expand Up @@ -334,6 +377,18 @@ impl Envelope {
EnvelopeItemIter { inner }
}

/// Returns the Envelope headers.
pub fn headers(&self) -> &EnvelopeHeaders {
&self.headers
}

/// Sets the Envelope headers.
#[must_use]
pub fn with_headers(mut self, headers: EnvelopeHeaders) -> Self {
self.headers = headers;
self
}

/// Returns the Envelopes Uuid, if any.
pub fn uuid(&self) -> Option<&Uuid> {
self.headers.event_id.as_ref()
Expand Down Expand Up @@ -646,7 +701,7 @@ mod test {

use super::*;
use crate::protocol::v7::{
Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes,
Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SampleRand, SessionAttributes,
SessionStatus, Span,
};

Expand Down Expand Up @@ -1039,6 +1094,21 @@ 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.999_999_9).unwrap()),
),
);
let expected = br#"{"trace":{"sample_rand":"0.999999"}}
"#;

let serialized = to_str(envelope);
assert_eq!(expected, serialized.as_bytes());
}

// Test all possible item types in a single envelope
#[test]
fn test_deserialize_serialized() {
Expand Down Expand Up @@ -1116,7 +1186,6 @@ some content
.into();

let mut envelope: Envelope = Envelope::new();

envelope.add_item(event);
envelope.add_item(transaction);
envelope.add_item(session);
Expand Down
116 changes: 106 additions & 10 deletions sentry-types/src/protocol/v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> for OrganizationId {
fn from(value: u64) -> Self {
Expand All @@ -2363,25 +2363,51 @@ 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<f64> 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<f64> for SampleRand {
type Error = InvalidSampleRandError;

fn try_from(value: f64) -> Result<Self, Self::Error> {
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<Self, Self::Err> {
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)
}
}
}

Expand All @@ -2392,8 +2418,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")]
Expand Down Expand Up @@ -2432,3 +2458,73 @@ pub(crate) struct DynamicSamplingContext {
)]
org_id: Option<OrganizationId>,
}

impl DynamicSamplingContext {
/// Creates an empty Dynamic Sampling Context.
pub fn new() -> Self {
Default::default()
}

/// Sets the trace ID.
#[must_use]
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: String) -> Self {
self.public_key = Some(public_key);
self
}

/// Sets the sample rate.
#[must_use]
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: SampleRand) -> Self {
self.sample_rand = Some(sample_rand);
self
}

/// Sets the sampled flag.
#[must_use]
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: String) -> Self {
self.release = Some(release);
self
}

/// Sets the environment.
#[must_use]
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: String) -> Self {
self.transaction = Some(transaction);
self
}

/// Sets the organization ID.
#[must_use]
pub fn with_org_id(mut self, org_id: OrganizationId) -> Self {
self.org_id = Some(org_id);
self
}
}
6 changes: 5 additions & 1 deletion sentry/src/transports/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ 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<()>),
Expand Down
6 changes: 5 additions & 1 deletion sentry/src/transports/tokio_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ 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<()>),
Expand Down
Loading