1use std::error::Error;
4use std::fmt::{Display, Formatter, Result as FmtResult};
5
6#[cfg(feature = "client")]
7use sentry_types::protocol::v7::OrganizationId;
8
9use crate::protocol::{SpanId, TraceId};
10
11type Header<'h> = (&'h str, &'h str);
13
14#[cfg(feature = "client")]
16const SENTRY_ORG_ID: &str = "sentry-org_id";
17
18const SENTRY_TRACE: &str = "sentry-trace";
20
21const BAGGAGE: &str = "baggage";
23
24#[derive(Debug, PartialEq, Clone, Default)]
38pub struct TracePropagationContext {
39 pub(crate) trace_id: TraceId,
40 pub(crate) span_id: SpanId,
41 pub(super) sampled: Option<bool>,
42 #[cfg(feature = "client")]
43 pub(super) org_id: Option<OrganizationId>,
44}
45
46#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub enum HeaderParseError {
50 Missing,
52 Invalid,
54}
55
56#[deprecated = "Please use `TracePropagationContext` instead"]
58#[derive(Debug, PartialEq, Clone, Copy, Default)]
59pub struct SentryTrace {
60 trace_id: TraceId,
61 span_id: SpanId,
62 sampled: Option<bool>,
63}
64
65impl TracePropagationContext {
66 pub fn new(trace_id: TraceId, span_id: SpanId) -> Self {
68 TracePropagationContext {
69 trace_id,
70 span_id,
71 sampled: None,
72 #[cfg(feature = "client")]
73 org_id: None,
74 }
75 }
76
77 pub fn with_sampled(self, sampled: bool) -> Self {
79 let sampled = Some(sampled);
80 Self { sampled, ..self }
81 }
82
83 pub fn sentry_trace_header(&self) -> String {
85 let Self {
86 trace_id,
87 span_id,
88 sampled,
89 #[cfg(feature = "client")]
90 org_id: _,
91 } = self;
92
93 let sampled_suffix = sampled
94 .map(|sampled| format!("-{}", if sampled { "1" } else { "0" }))
95 .unwrap_or_default();
96
97 format!("{trace_id}-{span_id}{sampled_suffix}")
98 }
99
100 pub fn try_from_headers<'a, I>(headers: I) -> Result<Self, HeaderParseError>
104 where
105 I: IntoIterator<Item = Header<'a>>,
106 {
107 let mut context_result = Err(HeaderParseError::Missing);
108 #[cfg(feature = "client")]
109 let mut baggage = SentryBaggage::default();
110
111 for (header, value) in headers {
112 if header.eq_ignore_ascii_case(SENTRY_TRACE) {
113 context_result = TracePropagationContext::from_sentry_trace(value)
116 .map_or(context_result, Ok)
117 .map_err(|_| HeaderParseError::Invalid);
118 } else if header.eq_ignore_ascii_case(BAGGAGE) {
119 #[cfg(feature = "client")]
120 baggage.update_from_header(value);
121 }
122 }
123
124 let context = context_result?;
125
126 #[cfg(feature = "client")]
127 let SentryBaggage { org_id } = baggage;
128 Ok(TracePropagationContext {
129 #[cfg(feature = "client")]
130 org_id,
131 ..context
132 })
133 }
134
135 pub(crate) fn with_maybe_sampled(self, sampled: Option<bool>) -> Self {
137 Self { sampled, ..self }
138 }
139
140 fn from_sentry_trace(header: &str) -> Option<Self> {
144 let header = header.trim();
145 let mut parts = header.splitn(3, '-');
146
147 let trace_id = parts.next()?.parse().ok()?;
148 let span_id = parts.next()?.parse().ok()?;
149 let sampled = parts.next().and_then(|sampled| match sampled {
150 "1" => Some(true),
151 "0" => Some(false),
152 _ => None,
153 });
154
155 Some(Self {
156 trace_id,
157 span_id,
158 sampled,
159 #[cfg(feature = "client")]
160 org_id: None,
161 })
162 }
163}
164
165#[deprecated = "use TracePropagationContext::try_from_headers instead"]
168#[expect(deprecated, reason = "backwards-compatible function")]
169pub fn parse_sentry_trace_header<'a, I>(headers: I) -> Option<SentryTrace>
170where
171 I: IntoIterator<Item = Header<'a>>,
172{
173 let TracePropagationContext {
174 trace_id,
175 span_id,
176 sampled,
177 #[cfg(feature = "client")]
178 org_id: _,
179 } = TracePropagationContext::try_from_headers(headers).ok()?;
180
181 Some(SentryTrace {
182 trace_id,
183 span_id,
184 sampled,
185 })
186}
187
188impl Display for HeaderParseError {
189 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
190 let msg = match self {
191 HeaderParseError::Missing => "missing",
192 HeaderParseError::Invalid => "invalid",
193 };
194
195 write!(f, "{msg} {SENTRY_TRACE} header")
196 }
197}
198
199impl Error for HeaderParseError {}
200
201#[expect(deprecated, reason = "backwards-compatible impl")]
202impl SentryTrace {
203 pub fn new(trace_id: TraceId, span_id: SpanId, sampled: Option<bool>) -> Self {
205 Self {
206 trace_id,
207 span_id,
208 sampled,
209 }
210 }
211}
212
213#[expect(deprecated, reason = "backwards-compatible impl")]
214impl From<SentryTrace> for TracePropagationContext {
215 fn from(trace: SentryTrace) -> Self {
216 Self {
217 trace_id: trace.trace_id,
218 span_id: trace.span_id,
219 sampled: trace.sampled,
220 #[cfg(feature = "client")]
221 org_id: None,
222 }
223 }
224}
225
226#[expect(deprecated, reason = "backwards-compatible impl")]
227impl std::fmt::Display for SentryTrace {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 write!(f, "{}-{}", self.trace_id, self.span_id)?;
230 if let Some(sampled) = self.sampled {
231 write!(f, "-{}", if sampled { '1' } else { '0' })?;
232 }
233 Ok(())
234 }
235}
236
237#[cfg(feature = "client")]
241#[derive(Debug, Default)]
242struct SentryBaggage {
243 org_id: Option<OrganizationId>,
244}
245
246#[cfg(feature = "client")]
247impl SentryBaggage {
248 fn update_from_header(&mut self, value: &str) {
256 value
257 .split(',')
258 .flat_map(|s| s.split_once('='))
259 .map(|(key, value)| (key, value.split_once(';').map_or(value, |(v, _)| v)))
261 .map(|(key, value)| (key.trim(), value.trim()))
262 .for_each(|(key, value)| self.update_from_value(key, value))
263 }
264
265 fn update_from_value(&mut self, key: &str, value: &str) {
269 if key == SENTRY_ORG_ID {
270 self.org_id = value.parse().ok().or(self.org_id);
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn parses_sentry_trace() {
281 let trace_id = "09e04486820349518ac7b5d2adbf6ba5".parse().unwrap();
282 let parent_trace_id = "9cf635fa5b870b3a".parse().unwrap();
283
284 let trace = TracePropagationContext::try_from_headers([(
285 "sentry-trace",
286 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
287 )])
288 .expect("should parse successfully");
289 assert_eq!(
290 trace,
291 TracePropagationContext {
292 trace_id,
293 span_id: parent_trace_id,
294 sampled: Some(false),
295 #[cfg(feature = "client")]
296 org_id: None,
297 }
298 );
299
300 let trace = TracePropagationContext::new(Default::default(), Default::default());
301 let parsed = TracePropagationContext::try_from_headers([(
302 "sentry-trace",
303 trace.sentry_trace_header().as_str(),
304 )])
305 .expect("should parse successfully");
306 assert_eq!(parsed, trace);
307 }
308
309 #[cfg(feature = "client")]
310 #[test]
311 fn parses_baggage_org_id() {
312 let trace = TracePropagationContext::try_from_headers([
313 (
314 "sentry-trace",
315 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
316 ),
317 ("baggage", "sentry-org_id=123"),
318 ])
319 .expect("should parse successfully");
320
321 assert_eq!(trace.org_id, Some("123".parse().unwrap()));
322 }
323
324 #[cfg(feature = "client")]
325 #[test]
326 fn parses_baggage_org_id_with_unrelated_fields() {
327 let trace = TracePropagationContext::try_from_headers([
328 (
329 "sentry-trace",
330 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
331 ),
332 (
333 "baggage",
334 "other=value, sentry-org_id=123, another=value;property",
335 ),
336 ])
337 .expect("should parse successfully");
338
339 assert_eq!(trace.org_id, Some("123".parse().unwrap()));
340 }
341
342 #[cfg(feature = "client")]
343 #[test]
344 fn accepts_mixed_case_baggage_header_name() {
345 let trace = TracePropagationContext::try_from_headers([
346 (
347 "sentry-trace",
348 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
349 ),
350 ("BagGaGe", "sentry-org_id=123"),
351 ])
352 .expect("should parse successfully");
353
354 assert_eq!(trace.org_id, Some("123".parse().unwrap()));
355 }
356
357 #[cfg(feature = "client")]
358 #[test]
359 fn treats_malformed_baggage_org_id_as_absent() {
360 let trace = TracePropagationContext::try_from_headers([
361 (
362 "sentry-trace",
363 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
364 ),
365 ("baggage", "sentry-org_id=not-an-org-id"),
366 ])
367 .expect("should parse successfully");
368
369 assert_eq!(trace.org_id, None);
370 }
371}