Skip to content
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
86 changes: 57 additions & 29 deletions sentry-types/src/protocol/envelope.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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 crate::utils::ts_rfc3339_opt;
use crate::Dsn;

use super::v7 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.
Expand Down Expand Up @@ -37,9 +40,23 @@ 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)]
Comment thread
lcian marked this conversation as resolved.
struct EnvelopeHeaders {
#[serde(default, skip_serializing_if = "Option::is_none")]
event_id: Option<Uuid>,
Comment thread
lcian marked this conversation as resolved.
#[serde(default, skip_serializing_if = "Option::is_none")]
dsn: Option<Dsn>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sdk: Option<ClientSdkInfo>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "ts_rfc3339_opt"
)]
sent_at: Option<SystemTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
trace: Option<DynamicSamplingContext>,
}

/// An Envelope Item Type.
Expand Down Expand Up @@ -271,7 +288,7 @@ impl Items {
/// for more details.
#[derive(Clone, Default, Debug, PartialEq)]
pub struct Envelope {
event_id: Option<Uuid>,
headers: EnvelopeHeaders,
items: Items,
}

Expand All @@ -297,11 +314,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);
Expand All @@ -319,7 +336,7 @@ impl Envelope {

/// 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.
Expand Down Expand Up @@ -391,11 +408,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)?;
Comment thread
lcian marked this conversation as resolved.
writeln!(writer)?;

let mut item_buf = Vec::new();
// write each item:
Expand Down Expand Up @@ -466,11 +480,11 @@ impl Envelope {

/// Creates a new Envelope from slice.
pub fn from_slice(slice: &[u8]) -> Result<Envelope, EnvelopeError> {
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()
};

Expand All @@ -484,8 +498,8 @@ impl Envelope {
/// Creates a new raw Envelope from the given buffer.
pub fn from_bytes_raw(bytes: Vec<u8>) -> Result<Self, EnvelopeError> {
Ok(Self {
event_id: None,
items: Items::Raw(bytes),
..Default::default()
})
}

Expand All @@ -504,19 +518,19 @@ 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();
fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> {
let first_line = slice
.split(|b| *b == b'\n')
.next()
.ok_or(EnvelopeError::MissingHeader)?;

let header: EnvelopeHeader = match stream.next() {
None => return Err(EnvelopeError::MissingHeader),
Some(Err(error)) => return Err(EnvelopeError::InvalidHeader(error)),
Some(Ok(header)) => header,
};
let headers: EnvelopeHeaders =
serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?;

// Each header is terminated by a UNIX newline.
Self::require_termination(slice, stream.byte_offset())?;
let offset = first_line.len();
Self::require_termination(slice, offset)?;

Ok((header, stream.byte_offset() + 1))
Ok((headers, offset + 1))
}

fn parse_items(slice: &[u8], mut offset: usize) -> Result<Vec<EnvelopeItem>, EnvelopeError> {
Expand Down Expand Up @@ -848,7 +862,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);
}

Expand Down Expand Up @@ -1011,6 +1025,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","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}
"#;

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() {
Expand Down
98 changes: 97 additions & 1 deletion sentry-types/src/protocol/v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -2336,3 +2336,99 @@ 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)]
struct OrganizationId(u64);

impl From<u64> for OrganizationId {
fn from(value: u64) -> Self {
Self(value)
}
}

impl std::str::FromStr for OrganizationId {
type Err = std::num::ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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)
}
}

/// 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<f64> 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<Self, Self::Err> {
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/).
///
/// 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(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
Comment thread
lcian marked this conversation as resolved.
#[serde(default, skip_serializing_if = "Option::is_none")]
trace_id: Option<TraceId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
public_key: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "display_from_str_opt"
Comment thread
lcian marked this conversation as resolved.
)]
sample_rate: Option<f32>,
// Required fields
Comment thread
lcian marked this conversation as resolved.
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "display_from_str_opt"
)]
sample_rand: Option<SampleRand>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "display_from_str_opt"
)]
sampled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
release: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
environment: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
transaction: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "display_from_str_opt"
)]
org_id: Option<OrganizationId>,
}
59 changes: 59 additions & 0 deletions sentry-types/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,65 @@ pub mod ts_rfc3339_opt {
}
}

/// Serialize and deserialize the inner value into/from a string using the `ToString`/`FromStr` implementation.
///
/// # Example
///
/// ```ignore
/// use serde::{Deserialize, Serialize};
///
/// #[derive(Debug, PartialEq, Serialize, Deserialize)]
/// struct Config {
/// #[serde(with = "sentry_types::utils::display_from_str_opt")]
/// host: Option<String>,
/// #[serde(with = "sentry_types::utils::display_from_str_opt")]
/// port: Option<u16>,
/// #[serde(with = "sentry_types::utils::display_from_str_opt")]
/// enabled: Option<bool>,
/// }
///
/// 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);
/// ```
Comment thread
lcian marked this conversation as resolved.
pub(crate) mod display_from_str_opt {
use serde::{de, ser, Deserialize};

pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
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<Option<T>, D::Error>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
D: de::Deserializer<'de>,
{
let opt_string = Option::<String>::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;
Expand Down
1 change: 1 addition & 0 deletions sentry/src/transports/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::time::Duration;
use super::ratelimit::{RateLimiter, RateLimitingCategory};
use crate::{sentry_debug, Envelope};

#[expect(clippy::large_enum_variant)]
Comment thread
lcian marked this conversation as resolved.
enum Task {
SendEnvelope(Envelope),
Comment thread
lcian marked this conversation as resolved.
Flush(SyncSender<()>),
Expand Down
1 change: 1 addition & 0 deletions sentry/src/transports/tokio_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment thread
lcian marked this conversation as resolved.
Flush(SyncSender<()>),
Expand Down
Loading