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 fn from_sentry_trace(header: &str) -> Option<Self> {
139 let header = header.trim();
140 let mut parts = header.splitn(3, '-');
141
142 let trace_id = parts.next()?.parse().ok()?;
143 let span_id = parts.next()?.parse().ok()?;
144 let sampled = parts.next().and_then(|sampled| match sampled {
145 "1" => Some(true),
146 "0" => Some(false),
147 _ => None,
148 });
149
150 Some(Self {
151 trace_id,
152 span_id,
153 sampled,
154 #[cfg(feature = "client")]
155 org_id: None,
156 })
157 }
158}
159
160#[deprecated = "use TracePropagationContext::try_from_headers instead"]
163#[expect(deprecated, reason = "backwards-compatible function")]
164pub fn parse_sentry_trace_header<'a, I>(headers: I) -> Option<SentryTrace>
165where
166 I: IntoIterator<Item = Header<'a>>,
167{
168 let TracePropagationContext {
169 trace_id,
170 span_id,
171 sampled,
172 #[cfg(feature = "client")]
173 org_id: _,
174 } = TracePropagationContext::try_from_headers(headers).ok()?;
175
176 Some(SentryTrace {
177 trace_id,
178 span_id,
179 sampled,
180 })
181}
182
183impl Display for HeaderParseError {
184 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
185 let msg = match self {
186 HeaderParseError::Missing => "missing",
187 HeaderParseError::Invalid => "invalid",
188 };
189
190 write!(f, "{msg} {SENTRY_TRACE} header")
191 }
192}
193
194impl Error for HeaderParseError {}
195
196#[expect(deprecated, reason = "backwards-compatible impl")]
197impl SentryTrace {
198 pub fn new(trace_id: TraceId, span_id: SpanId, sampled: Option<bool>) -> Self {
200 Self {
201 trace_id,
202 span_id,
203 sampled,
204 }
205 }
206}
207
208#[expect(deprecated, reason = "backwards-compatible impl")]
209impl From<SentryTrace> for TracePropagationContext {
210 fn from(trace: SentryTrace) -> Self {
211 Self {
212 trace_id: trace.trace_id,
213 span_id: trace.span_id,
214 sampled: trace.sampled,
215 #[cfg(feature = "client")]
216 org_id: None,
217 }
218 }
219}
220
221#[expect(deprecated, reason = "backwards-compatible impl")]
222impl std::fmt::Display for SentryTrace {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "{}-{}", self.trace_id, self.span_id)?;
225 if let Some(sampled) = self.sampled {
226 write!(f, "-{}", if sampled { '1' } else { '0' })?;
227 }
228 Ok(())
229 }
230}
231
232#[cfg(feature = "client")]
236#[derive(Debug, Default)]
237struct SentryBaggage {
238 org_id: Option<OrganizationId>,
239}
240
241#[cfg(feature = "client")]
242impl SentryBaggage {
243 fn update_from_header(&mut self, value: &str) {
251 value
252 .split(',')
253 .flat_map(|s| s.split_once('='))
254 .map(|(key, value)| (key, value.split_once(';').map_or(value, |(v, _)| v)))
256 .map(|(key, value)| (key.trim(), value.trim()))
257 .for_each(|(key, value)| self.update_from_value(key, value))
258 }
259
260 fn update_from_value(&mut self, key: &str, value: &str) {
264 if key == SENTRY_ORG_ID {
265 self.org_id = value.parse().ok().or(self.org_id);
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn parses_sentry_trace() {
276 let trace_id = "09e04486820349518ac7b5d2adbf6ba5".parse().unwrap();
277 let parent_trace_id = "9cf635fa5b870b3a".parse().unwrap();
278
279 let trace = TracePropagationContext::try_from_headers([(
280 "sentry-trace",
281 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
282 )])
283 .expect("should parse successfully");
284 assert_eq!(
285 trace,
286 TracePropagationContext {
287 trace_id,
288 span_id: parent_trace_id,
289 sampled: Some(false),
290 #[cfg(feature = "client")]
291 org_id: None,
292 }
293 );
294
295 let trace = TracePropagationContext::new(Default::default(), Default::default());
296 let parsed = TracePropagationContext::try_from_headers([(
297 "sentry-trace",
298 trace.sentry_trace_header().as_str(),
299 )])
300 .expect("should parse successfully");
301 assert_eq!(parsed, trace);
302 }
303
304 #[cfg(feature = "client")]
305 #[test]
306 fn parses_baggage_org_id() {
307 let trace = TracePropagationContext::try_from_headers([
308 (
309 "sentry-trace",
310 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
311 ),
312 ("baggage", "sentry-org_id=123"),
313 ])
314 .expect("should parse successfully");
315
316 assert_eq!(trace.org_id, Some("123".parse().unwrap()));
317 }
318
319 #[cfg(feature = "client")]
320 #[test]
321 fn parses_baggage_org_id_with_unrelated_fields() {
322 let trace = TracePropagationContext::try_from_headers([
323 (
324 "sentry-trace",
325 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
326 ),
327 (
328 "baggage",
329 "other=value, sentry-org_id=123, another=value;property",
330 ),
331 ])
332 .expect("should parse successfully");
333
334 assert_eq!(trace.org_id, Some("123".parse().unwrap()));
335 }
336
337 #[cfg(feature = "client")]
338 #[test]
339 fn accepts_mixed_case_baggage_header_name() {
340 let trace = TracePropagationContext::try_from_headers([
341 (
342 "sentry-trace",
343 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
344 ),
345 ("BagGaGe", "sentry-org_id=123"),
346 ])
347 .expect("should parse successfully");
348
349 assert_eq!(trace.org_id, Some("123".parse().unwrap()));
350 }
351
352 #[cfg(feature = "client")]
353 #[test]
354 fn treats_malformed_baggage_org_id_as_absent() {
355 let trace = TracePropagationContext::try_from_headers([
356 (
357 "sentry-trace",
358 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-0",
359 ),
360 ("baggage", "sentry-org_id=not-an-org-id"),
361 ])
362 .expect("should parse successfully");
363
364 assert_eq!(trace.org_id, None);
365 }
366}