Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### New Features

- Added [`TracePropagationContext`](https://docs.rs/sentry-core/latest/sentry_core/struct.TracePropagationContext.html) as the preferred type for Sentry trace propagation metadata. The existing [`SentryTrace`](https://docs.rs/sentry-core/latest/sentry_core/struct.SentryTrace.html) type remains available for backwards compatibility ([#1212](https://github.com/getsentry/sentry-rust/pull/1212)).
- Added [`Dsn::org_id`](https://docs.rs/sentry-types/latest/sentry_types/struct.Dsn.html#method.org_id), which parses the Sentry SaaS organization ID from DSN hosts such as `o123.ingest.sentry.io` ([#1202](https://github.com/getsentry/sentry-rust/pull/1202)).

## 0.48.3
Expand Down
230 changes: 183 additions & 47 deletions sentry-core/src/performance/headers.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,185 @@
//! Module containing utilities for interacting with Sentry tracing headers.

use crate::protocol;
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};

/// A container for distributed tracing metadata that can be extracted from e.g. the `sentry-trace`
/// HTTP header.
use crate::protocol::{SpanId, TraceId};

/// A key-value header pair.
type Header<'h> = (&'h str, &'h str);

/// The [trace propagation] context.
///
/// Contains the information necessary for propagating Sentry traces and continuing traces from
/// incoming requests.
///
/// The data stored in this struct can be parsed from and transmitted as `sentry-trace` headers.
///
/// Note that the Rust SDK only partially supports trace propagation, certain features such as
/// [dynamic sampling] may be missing or incomplete.
///
/// [trace propagation]: https://develop.sentry.dev/sdk/foundations/trace-propagation/
/// [dynamic sampling]: https://develop.sentry.dev/sdk/foundations/trace-propagation/dynamic-sampling-context/
#[derive(Debug, PartialEq, Clone, Default)]
pub struct TracePropagationContext {
pub(crate) trace_id: TraceId,
pub(crate) span_id: SpanId,
pub(super) sampled: Option<bool>,
}

#[derive(Debug, Clone)]
#[non_exhaustive]
/// Error type returned by [`TracePropagationContext::try_from_headers`].
pub enum HeaderParseError {
/// The `sentry-trace` header was missing.
Missing,
/// There was a `sentry-trace` header, but it was invalid.
Invalid,
}

/// A container for `sentry-trace` data.
#[deprecated = "Please use `TracePropagationContext` instead"]
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub struct SentryTrace {
pub(crate) trace_id: protocol::TraceId,
pub(crate) span_id: protocol::SpanId,
pub(crate) sampled: Option<bool>,
trace_id: TraceId,
span_id: SpanId,
sampled: Option<bool>,
}

impl SentryTrace {
/// Creates a new [`SentryTrace`] from the provided parameters
pub fn new(
trace_id: protocol::TraceId,
span_id: protocol::SpanId,
sampled: Option<bool>,
) -> Self {
SentryTrace {
impl TracePropagationContext {
/// Creates a new [`TracePropagationContext`] from the provided parameters
pub fn new(trace_id: TraceId, span_id: SpanId) -> Self {
TracePropagationContext {
trace_id,
span_id,
sampled: None,
}
}

/// Set the sampling decision on `self`.
pub fn with_sampled(self, sampled: bool) -> Self {
let sampled = Some(sampled);
Self { sampled, ..self }
}

/// Computes the `sentry-trace` header for this [`TracePropagationContext`].
pub fn sentry_trace_header(&self) -> String {
let Self {
trace_id,
span_id,
sampled,
} = self;

let sampled_suffix = sampled
.map(|sampled| format!("-{}", if sampled { "1" } else { "0" }))
.unwrap_or_default();

format!("{trace_id}-{span_id}{sampled_suffix}")
}

/// Attempt to parse a list of Sentry headers into [`TracePropagationContext`].
///
/// The parsing will fail if there is no valid `sentry-trace` header.
pub fn try_from_headers<'a, I>(headers: I) -> Result<Self, HeaderParseError>
where
I: IntoIterator<Item = Header<'a>>,
{
let mut context_result = Err(HeaderParseError::Missing);

for (header, value) in headers {
if header.eq_ignore_ascii_case("sentry-trace") {
// Parse the header, falling back to the previous header value if Ok (headers not
// guaranteed unique), only falling back to invalid error if there's no prev value.
context_result = TracePropagationContext::from_sentry_trace(value)
.map_or(context_result, Ok)
.map_err(|_| HeaderParseError::Invalid);
}
}

context_result
}

/// Attempts to construct a [`TracePropagationContext`] from the given Sentry trace header.
///
/// Returns [`None`] if the header cannot be parsed.
fn from_sentry_trace(header: &str) -> Option<Self> {
let header = header.trim();
let mut parts = header.splitn(3, '-');

let trace_id = parts.next()?.parse().ok()?;
let span_id = parts.next()?.parse().ok()?;
let sampled = parts.next().and_then(|sampled| match sampled {
"1" => Some(true),
"0" => Some(false),
_ => None,
});

Some(Self {
trace_id,
span_id,
sampled,
})
}
}

fn parse_sentry_trace(header: &str) -> Option<SentryTrace> {
let header = header.trim();
let mut parts = header.splitn(3, '-');
/// Extracts distributed tracing metadata from headers (or, generally, key-value pairs),
/// considering the values for `sentry-trace`.
#[deprecated = "use TracePropagationContext::try_from_headers instead"]
#[expect(deprecated, reason = "backwards-compatible function")]
pub fn parse_sentry_trace_header<'a, I>(headers: I) -> Option<SentryTrace>
where
I: IntoIterator<Item = Header<'a>>,
{
let TracePropagationContext {
trace_id,
span_id,
sampled,
} = TracePropagationContext::try_from_headers(headers).ok()?;

let trace_id = parts.next()?.parse().ok()?;
let parent_span_id = parts.next()?.parse().ok()?;
let parent_sampled = parts.next().and_then(|sampled| match sampled {
"1" => Some(true),
"0" => Some(false),
_ => None,
});
Some(SentryTrace {
trace_id,
span_id,
sampled,
})
}

Some(SentryTrace::new(trace_id, parent_span_id, parent_sampled))
impl Display for HeaderParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let msg = match self {
HeaderParseError::Missing => "missing",
HeaderParseError::Invalid => "invalid",
};

write!(f, "{msg} sentry-trace header")
}
}

impl Error for HeaderParseError {}

#[expect(deprecated, reason = "backwards-compatible impl")]
impl SentryTrace {
/// Creates a new [`SentryTrace`] from the provided parameters
pub fn new(trace_id: TraceId, span_id: SpanId, sampled: Option<bool>) -> Self {
Self {
trace_id,
span_id,
sampled,
}
}
}

#[expect(deprecated, reason = "backwards-compatible impl")]
impl From<SentryTrace> for TracePropagationContext {
fn from(trace: SentryTrace) -> Self {
Self {
trace_id: trace.trace_id,
span_id: trace.span_id,
sampled: trace.sampled,
}
}
}

#[expect(deprecated, reason = "backwards-compatible impl")]
impl std::fmt::Display for SentryTrace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}-{}", self.trace_id, self.span_id)?;
Expand All @@ -51,38 +190,35 @@ impl std::fmt::Display for SentryTrace {
}
}

/// Extracts distributed tracing metadata from headers (or, generally, key-value pairs),
/// considering the values for `sentry-trace`.
pub fn parse<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(headers: I) -> Option<SentryTrace> {
let mut trace = None;
for (k, v) in headers.into_iter() {
if k.eq_ignore_ascii_case("sentry-trace") {
trace = parse_sentry_trace(v);
break;
}
}
trace
}

#[cfg(test)]
mod tests {
use std::str::FromStr as _;

use super::*;

#[test]
fn parses_sentry_trace() {
let trace_id = protocol::TraceId::from_str("09e04486820349518ac7b5d2adbf6ba5").unwrap();
let parent_trace_id = protocol::SpanId::from_str("9cf635fa5b870b3a").unwrap();
let trace_id = "09e04486820349518ac7b5d2adbf6ba5".parse().unwrap();
let parent_trace_id = "9cf635fa5b870b3a".parse().unwrap();

let trace = parse_sentry_trace("09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0");
let trace = TracePropagationContext::try_from_headers([(
"sentry-trace",
"09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
)])
.expect("should parse successfully");
assert_eq!(
trace,
Some(SentryTrace::new(trace_id, parent_trace_id, Some(false)))
TracePropagationContext {
trace_id,
span_id: parent_trace_id,
sampled: Some(false),
}
);

let trace = SentryTrace::new(Default::default(), Default::default(), None);
let parsed = parse_sentry_trace(&trace.to_string());
assert_eq!(parsed, Some(trace));
let trace = TracePropagationContext::new(Default::default(), Default::default());
let parsed = TracePropagationContext::try_from_headers([(
"sentry-trace",
trace.sentry_trace_header().as_str(),
)])
.expect("should parse successfully");
assert_eq!(parsed, trace);
}
}
53 changes: 37 additions & 16 deletions sentry-core/src/performance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use crate::{protocol, Hub};
#[cfg(feature = "client")]
use crate::Client;

pub use self::headers::{parse as parse_headers, SentryTrace};
#[expect(deprecated, reason = "backwards-compatibility re-export")]
pub use self::headers::{parse_sentry_trace_header as parse_headers, SentryTrace};
pub use self::headers::{HeaderParseError, TracePropagationContext};

mod headers;

Expand Down Expand Up @@ -184,9 +186,9 @@ impl TransactionContext {
op: &str,
headers: I,
) -> Self {
parse_headers(headers)
.map(|sentry_trace| Self::continue_from_sentry_trace(name, op, &sentry_trace, None))
.unwrap_or_else(|| Self {
TracePropagationContext::try_from_headers(headers)
.map(|context| Self::continue_from_trace_propagation_context(name, op, &context, None))
.unwrap_or_else(|_| Self {
name: name.into(),
op: op.into(),
trace_id: Default::default(),
Expand All @@ -199,18 +201,38 @@ impl TransactionContext {

/// Creates a new Transaction Context based on the provided distributed tracing data,
/// optionally creating the `TransactionContext` with the provided `span_id`.
#[deprecated = "use `TransactionContext::continue_from_trace_propagation_context` instead"]
#[expect(deprecated, reason = "backwards-compatible method")]
pub fn continue_from_sentry_trace(
name: &str,
op: &str,
sentry_trace: &SentryTrace,
span_id: Option<SpanId>,
) -> Self {
let context = (*sentry_trace).into();
Self::continue_from_trace_propagation_context(name, op, &context, span_id)
}

/// Creates a new Transaction Context based on the provided trace propagation context,
/// optionally creating the `TransactionContext` with the provided `span_id`.
pub fn continue_from_trace_propagation_context(
name: &str,
op: &str,
context: &TracePropagationContext,
span_id: Option<SpanId>,
) -> Self {
let &TracePropagationContext {
trace_id,
span_id: context_span_id,
sampled,
} = context;

Self {
name: name.into(),
op: op.into(),
trace_id: sentry_trace.trace_id,
parent_span_id: Some(sentry_trace.span_id),
sampled: sentry_trace.sampled,
trace_id,
parent_span_id: Some(context_span_id),
sampled,
span_id: span_id.unwrap_or_default(),
custom: None,
}
Expand Down Expand Up @@ -836,13 +858,10 @@ impl Transaction {
/// trace's distributed tracing headers.
pub fn iter_headers(&self) -> TraceHeadersIter {
let inner = self.inner.lock().unwrap();
let trace = SentryTrace::new(
inner.context.trace_id,
inner.context.span_id,
Some(inner.sampled),
);
let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
.with_sampled(inner.sampled);
TraceHeadersIter {
sentry_trace: Some(trace.to_string()),
sentry_trace: Some(trace.sentry_trace_header()),
}
}

Expand Down Expand Up @@ -1123,9 +1142,10 @@ impl Span {
/// trace's distributed tracing headers.
pub fn iter_headers(&self) -> TraceHeadersIter {
let span = self.span.lock().unwrap();
let trace = SentryTrace::new(span.trace_id, span.span_id, Some(self.sampled));
let trace =
TracePropagationContext::new(span.trace_id, span.span_id).with_sampled(self.sampled);
TraceHeadersIter {
sentry_trace: Some(trace.to_string()),
sentry_trace: Some(trace.sentry_trace_header()),
}
}

Expand Down Expand Up @@ -1272,7 +1292,8 @@ mod tests {
let span = trx.start_child("noop", "noop");

let header = span.iter_headers().next().unwrap().1;
let parsed = parse_headers([("sentry-trace", header.as_str())]).unwrap();
let parsed =
TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();

assert_eq!(
&parsed.trace_id.to_string(),
Expand Down
Loading
Loading