Skip to main content

sentry_core/performance/
mod.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::ops::{Deref, DerefMut};
4use std::sync::{Arc, Mutex, MutexGuard};
5use std::time::SystemTime;
6
7#[cfg(feature = "client")]
8use sentry_types::protocol::v7::client_report::Reason as ClientReportReason;
9#[cfg(feature = "client")]
10use sentry_types::protocol::v7::OrganizationId;
11use sentry_types::protocol::v7::SpanId;
12
13#[cfg(feature = "client")]
14use self::sampling::FinishAction;
15use self::sampling::TracingState;
16#[cfg(feature = "client")]
17use crate::clientoptions::TracesSamplingStrategy;
18use crate::{protocol, Hub};
19
20#[cfg(feature = "client")]
21use crate::Client;
22
23#[expect(deprecated, reason = "backwards-compatibility re-export")]
24pub use self::headers::{parse_sentry_trace_header as parse_headers, SentryTrace};
25pub use self::headers::{HeaderParseError, TracePropagationContext};
26
27mod headers;
28mod sampling;
29
30#[cfg(feature = "client")]
31const MAX_SPANS: usize = 1_000;
32
33// global API:
34
35/// Start a new Performance Monitoring Transaction.
36///
37/// The transaction needs to be explicitly finished via [`Transaction::finish`],
38/// otherwise it will be discarded.
39/// The transaction itself also represents the root span in the span hierarchy.
40/// Child spans can be started with the [`Transaction::start_child`] method.
41pub fn start_transaction(ctx: TransactionContext) -> Transaction {
42    #[cfg(feature = "client")]
43    {
44        let client = Hub::with_active(|hub| hub.client());
45        Transaction::new(client, ctx)
46    }
47    #[cfg(not(feature = "client"))]
48    {
49        Transaction::new_noop(ctx)
50    }
51}
52
53/// Start a new Performance Monitoring Transaction with the provided start timestamp.
54///
55/// The transaction needs to be explicitly finished via [`Transaction::finish`],
56/// otherwise it will be discarded.
57/// The transaction itself also represents the root span in the span hierarchy.
58/// Child spans can be started with the [`Transaction::start_child`] method.
59pub fn start_transaction_with_timestamp(
60    ctx: TransactionContext,
61    timestamp: SystemTime,
62) -> Transaction {
63    let transaction = start_transaction(ctx);
64    if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
65        tx.start_timestamp = timestamp;
66    }
67    transaction
68}
69
70// Hub API:
71
72impl Hub {
73    /// Start a new Performance Monitoring Transaction.
74    ///
75    /// See the global [`start_transaction`] for more documentation.
76    pub fn start_transaction(&self, ctx: TransactionContext) -> Transaction {
77        #[cfg(feature = "client")]
78        {
79            Transaction::new(self.client(), ctx)
80        }
81        #[cfg(not(feature = "client"))]
82        {
83            Transaction::new_noop(ctx)
84        }
85    }
86
87    /// Start a new Performance Monitoring Transaction with the provided start timestamp.
88    ///
89    /// See the global [`start_transaction_with_timestamp`] for more documentation.
90    pub fn start_transaction_with_timestamp(
91        &self,
92        ctx: TransactionContext,
93        timestamp: SystemTime,
94    ) -> Transaction {
95        let transaction = start_transaction(ctx);
96        if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
97            tx.start_timestamp = timestamp;
98        }
99        transaction
100    }
101}
102
103// "Context" Types:
104
105/// Arbitrary data passed by the caller, when starting a transaction.
106///
107/// May be inspected by the user in the `traces_sampler` callback, if set.
108///
109/// Represents arbitrary JSON data, the top level of which must be a map.
110pub type CustomTransactionContext = serde_json::Map<String, serde_json::Value>;
111
112/// Information from an incoming trace.
113///
114/// Currently this just contains the org ID supplied by the incoming trace.
115#[cfg(feature = "client")]
116#[derive(Debug, Clone, Copy)]
117struct IncomingTrace {
118    org_id: Option<OrganizationId>,
119}
120
121/// The Transaction Context used to start a new Performance Monitoring Transaction.
122///
123/// The Transaction Context defines the metadata for a Performance Monitoring
124/// Transaction, and also the connection point for distributed tracing.
125#[derive(Debug, Clone)]
126pub struct TransactionContext {
127    #[cfg_attr(not(feature = "client"), allow(dead_code))]
128    name: String,
129    op: String,
130    trace_id: protocol::TraceId,
131    parent_span_id: Option<protocol::SpanId>,
132    span_id: protocol::SpanId,
133    sampled: Option<bool>,
134    #[cfg(feature = "client")]
135    incoming_trace: Option<IncomingTrace>,
136    custom: Option<CustomTransactionContext>,
137}
138
139impl TransactionContext {
140    /// Creates a new Transaction Context with the given `name` and `op`. A random
141    /// `trace_id` is assigned. Use [`TransactionContext::new_with_trace_id`] to
142    /// specify a custom trace ID.
143    ///
144    /// See <https://docs.sentry.io/platforms/native/enriching-events/transaction-name/>
145    /// for an explanation of a Transaction's `name`, and
146    /// <https://develop.sentry.dev/sdk/performance/span-operations/> for conventions
147    /// around an `operation`'s value.
148    ///
149    /// See also the [`TransactionContext::continue_from_headers`] function that
150    /// can be used for distributed tracing.
151    #[must_use = "this must be used with `start_transaction`"]
152    pub fn new(name: &str, op: &str) -> Self {
153        Self::new_with_trace_id(name, op, protocol::TraceId::default())
154    }
155
156    /// Creates a new Transaction Context with the given `name`, `op`, and `trace_id`.
157    ///
158    /// See <https://docs.sentry.io/platforms/native/enriching-events/transaction-name/>
159    /// for an explanation of a Transaction's `name`, and
160    /// <https://develop.sentry.dev/sdk/performance/span-operations/> for conventions
161    /// around an `operation`'s value.
162    #[must_use = "this must be used with `start_transaction`"]
163    pub fn new_with_trace_id(name: &str, op: &str, trace_id: protocol::TraceId) -> Self {
164        Self {
165            name: name.into(),
166            op: op.into(),
167            trace_id,
168            parent_span_id: None,
169            span_id: Default::default(),
170            sampled: None,
171            #[cfg(feature = "client")]
172            incoming_trace: None,
173            custom: None,
174        }
175    }
176
177    /// Creates a new Transaction Context with the given `name`, `op`, `trace_id`, and
178    /// possibly the given `span_id` and `parent_span_id`.
179    ///
180    /// See <https://docs.sentry.io/platforms/native/enriching-events/transaction-name/>
181    /// for an explanation of a Transaction's `name`, and
182    /// <https://develop.sentry.dev/sdk/performance/span-operations/> for conventions
183    /// around an `operation`'s value.
184    #[must_use = "this must be used with `start_transaction`"]
185    pub fn new_with_details(
186        name: &str,
187        op: &str,
188        trace_id: protocol::TraceId,
189        span_id: Option<protocol::SpanId>,
190        parent_span_id: Option<protocol::SpanId>,
191    ) -> Self {
192        let mut slf = Self::new_with_trace_id(name, op, trace_id);
193        if let Some(span_id) = span_id {
194            slf.span_id = span_id;
195        }
196        slf.parent_span_id = parent_span_id;
197        slf
198    }
199
200    /// Creates a new Transaction Context based on the distributed tracing `headers`.
201    ///
202    /// The `headers` in particular need to include the `sentry-trace` header,
203    /// which is used to associate the transaction with a distributed trace.
204    #[must_use = "this must be used with `start_transaction`"]
205    pub fn continue_from_headers<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(
206        name: &str,
207        op: &str,
208        headers: I,
209    ) -> Self {
210        TracePropagationContext::try_from_headers(headers)
211            .map(|context| Self::continue_from_trace_propagation_context(name, op, &context, None))
212            .unwrap_or_else(|_| Self {
213                name: name.into(),
214                op: op.into(),
215                trace_id: Default::default(),
216                parent_span_id: None,
217                span_id: Default::default(),
218                sampled: None,
219                #[cfg(feature = "client")]
220                incoming_trace: None,
221                custom: None,
222            })
223    }
224
225    /// Creates a new Transaction Context based on the provided distributed tracing data,
226    /// optionally creating the `TransactionContext` with the provided `span_id`.
227    #[deprecated = "use `TransactionContext::continue_from_trace_propagation_context` instead"]
228    #[expect(deprecated, reason = "backwards-compatible method")]
229    pub fn continue_from_sentry_trace(
230        name: &str,
231        op: &str,
232        sentry_trace: &SentryTrace,
233        span_id: Option<SpanId>,
234    ) -> Self {
235        let context = (*sentry_trace).into();
236        Self::continue_from_trace_propagation_context(name, op, &context, span_id)
237    }
238
239    /// Creates a new Transaction Context based on the provided trace propagation context,
240    /// optionally creating the `TransactionContext` with the provided `span_id`.
241    pub fn continue_from_trace_propagation_context(
242        name: &str,
243        op: &str,
244        context: &TracePropagationContext,
245        span_id: Option<SpanId>,
246    ) -> Self {
247        let &TracePropagationContext {
248            trace_id,
249            span_id: context_span_id,
250            sampled,
251            #[cfg(feature = "client")]
252            org_id,
253        } = context;
254
255        Self {
256            name: name.into(),
257            op: op.into(),
258            trace_id,
259            parent_span_id: Some(context_span_id),
260            sampled,
261            #[cfg(feature = "client")]
262            incoming_trace: Some(IncomingTrace { org_id }),
263            span_id: span_id.unwrap_or_default(),
264            custom: None,
265        }
266    }
267
268    /// Creates a new Transaction Context based on an existing Span.
269    ///
270    /// This should be used when an independent computation is spawned on another
271    /// thread and should be connected to the calling thread via a distributed
272    /// tracing transaction.
273    pub fn continue_from_span(name: &str, op: &str, span: Option<TransactionOrSpan>) -> Self {
274        let span = match span {
275            Some(span) => span,
276            None => return Self::new(name, op),
277        };
278
279        let (trace_id, parent_span_id, sampled) = match span {
280            TransactionOrSpan::Transaction(transaction) => {
281                let inner = transaction.inner.lock().unwrap();
282                (
283                    inner.context.trace_id,
284                    inner.context.span_id,
285                    inner.tracing_state.trace_sampled(),
286                )
287            }
288            TransactionOrSpan::Span(span) => {
289                let trace_sampled = span.tracing_state.trace_sampled();
290                let span = span.span.lock().unwrap();
291                (span.trace_id, span.span_id, trace_sampled)
292            }
293        };
294
295        Self {
296            name: name.into(),
297            op: op.into(),
298            trace_id,
299            parent_span_id: Some(parent_span_id),
300            span_id: protocol::SpanId::default(),
301            sampled,
302            #[cfg(feature = "client")]
303            incoming_trace: None,
304            custom: None,
305        }
306    }
307
308    /// Set the sampling decision for this Transaction.
309    ///
310    /// This can be either an explicit boolean flag, or [`None`], which leaves
311    /// the decision to the configured traces sampling strategy.
312    #[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")]
313    pub fn set_sampled(&mut self, sampled: impl Into<Option<bool>>) {
314        self.sampled = sampled.into();
315    }
316
317    /// Get the sampling decision for this Transaction.
318    pub fn sampled(&self) -> Option<bool> {
319        self.sampled
320    }
321
322    /// Get the name of this Transaction.
323    pub fn name(&self) -> &str {
324        &self.name
325    }
326
327    /// Get the operation of this Transaction.
328    pub fn operation(&self) -> &str {
329        &self.op
330    }
331
332    /// Get the Trace ID of this Transaction.
333    pub fn trace_id(&self) -> protocol::TraceId {
334        self.trace_id
335    }
336
337    /// Get the Span ID of this Transaction.
338    pub fn span_id(&self) -> protocol::SpanId {
339        self.span_id
340    }
341
342    /// Get the custom context of this Transaction.
343    pub fn custom(&self) -> Option<&CustomTransactionContext> {
344        self.custom.as_ref()
345    }
346
347    /// Update the custom context of this Transaction.
348    ///
349    /// For simply adding a key, use the `custom_insert` method.
350    pub fn custom_mut(&mut self) -> &mut Option<CustomTransactionContext> {
351        &mut self.custom
352    }
353
354    /// Inserts a key-value pair into the custom context.
355    ///
356    /// If the context did not have this key present, None is returned.
357    ///
358    /// If the context did have this key present, the value is updated, and the old value is
359    /// returned.
360    pub fn custom_insert(
361        &mut self,
362        key: String,
363        value: serde_json::Value,
364    ) -> Option<serde_json::Value> {
365        // Get the custom context
366        let mut custom = None;
367        std::mem::swap(&mut self.custom, &mut custom);
368
369        // Initialise the context, if not used yet
370        let mut custom = custom.unwrap_or_default();
371
372        // And set our key
373        let existing_value = custom.insert(key, value);
374        self.custom = Some(custom);
375        existing_value
376    }
377
378    /// Creates a transaction context builder initialized with the given `name` and `op`.
379    ///
380    /// See <https://docs.sentry.io/platforms/native/enriching-events/transaction-name/>
381    /// for an explanation of a Transaction's `name`, and
382    /// <https://develop.sentry.dev/sdk/performance/span-operations/> for conventions
383    /// around an `operation`'s value.
384    #[must_use]
385    pub fn builder(name: &str, op: &str) -> TransactionContextBuilder {
386        TransactionContextBuilder {
387            ctx: TransactionContext::new(name, op),
388        }
389    }
390
391    /// Clears incoming trace state so the transaction starts a new trace.
392    #[cfg(feature = "client")]
393    fn reject_incoming_trace(&mut self) {
394        (
395            self.trace_id,
396            self.parent_span_id,
397            self.sampled,
398            self.incoming_trace,
399        ) = Default::default();
400    }
401}
402
403/// A transaction context builder created by [`TransactionContext::builder`].
404pub struct TransactionContextBuilder {
405    ctx: TransactionContext,
406}
407
408impl TransactionContextBuilder {
409    /// Defines the name of the transaction.
410    #[must_use]
411    pub fn with_name(mut self, name: String) -> Self {
412        self.ctx.name = name;
413        self
414    }
415
416    /// Defines the operation of the transaction.
417    #[must_use]
418    pub fn with_op(mut self, op: String) -> Self {
419        self.ctx.op = op;
420        self
421    }
422
423    /// Defines the trace ID.
424    #[must_use]
425    pub fn with_trace_id(mut self, trace_id: protocol::TraceId) -> Self {
426        self.ctx.trace_id = trace_id;
427        self
428    }
429
430    /// Defines a parent span ID for the created transaction.
431    #[must_use]
432    pub fn with_parent_span_id(mut self, parent_span_id: Option<protocol::SpanId>) -> Self {
433        self.ctx.parent_span_id = parent_span_id;
434        self
435    }
436
437    /// Defines the span ID to be used when creating the transaction.
438    #[must_use]
439    pub fn with_span_id(mut self, span_id: protocol::SpanId) -> Self {
440        self.ctx.span_id = span_id;
441        self
442    }
443
444    /// Defines whether the transaction will be sampled.
445    #[must_use]
446    pub fn with_sampled(mut self, sampled: Option<bool>) -> Self {
447        self.ctx.sampled = sampled;
448        self
449    }
450
451    /// Adds a custom key and value to the transaction context.
452    #[must_use]
453    pub fn with_custom(mut self, key: String, value: serde_json::Value) -> Self {
454        self.ctx.custom_insert(key, value);
455        self
456    }
457
458    /// Finishes building a transaction.
459    pub fn finish(self) -> TransactionContext {
460        self.ctx
461    }
462}
463
464/// A function to be run for each new transaction, to determine the rate at which
465/// it should be sampled.
466///
467/// This function may choose to respect the sampling of the parent transaction (`ctx.sampled`)
468/// or ignore it.
469pub type TracesSampler = dyn Fn(&TransactionContext) -> f32 + Send + Sync;
470
471// global API types:
472
473/// A wrapper that groups a [`Transaction`] and a [`Span`] together.
474#[derive(Clone, Debug, PartialEq)]
475pub enum TransactionOrSpan {
476    /// A [`Transaction`].
477    Transaction(Transaction),
478    /// A [`Span`].
479    Span(Span),
480}
481
482impl From<Transaction> for TransactionOrSpan {
483    fn from(transaction: Transaction) -> Self {
484        Self::Transaction(transaction)
485    }
486}
487
488impl From<Span> for TransactionOrSpan {
489    fn from(span: Span) -> Self {
490        Self::Span(span)
491    }
492}
493
494impl TransactionOrSpan {
495    /// Set some extra information to be sent with this Transaction/Span.
496    pub fn set_data(&self, key: &str, value: protocol::Value) {
497        match self {
498            TransactionOrSpan::Transaction(transaction) => transaction.set_data(key, value),
499            TransactionOrSpan::Span(span) => span.set_data(key, value),
500        }
501    }
502
503    /// Sets a tag to a specific value.
504    pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
505        match self {
506            TransactionOrSpan::Transaction(transaction) => transaction.set_tag(key, value),
507            TransactionOrSpan::Span(span) => span.set_tag(key, value),
508        }
509    }
510
511    /// Get the TransactionContext of the Transaction/Span.
512    ///
513    /// Note that this clones the underlying value.
514    pub fn get_trace_context(&self) -> protocol::TraceContext {
515        match self {
516            TransactionOrSpan::Transaction(transaction) => transaction.get_trace_context(),
517            TransactionOrSpan::Span(span) => span.get_trace_context(),
518        }
519    }
520
521    /// Set the status of the Transaction/Span.
522    pub fn get_status(&self) -> Option<protocol::SpanStatus> {
523        match self {
524            TransactionOrSpan::Transaction(transaction) => transaction.get_status(),
525            TransactionOrSpan::Span(span) => span.get_status(),
526        }
527    }
528
529    /// Set the status of the Transaction/Span.
530    pub fn set_status(&self, status: protocol::SpanStatus) {
531        match self {
532            TransactionOrSpan::Transaction(transaction) => transaction.set_status(status),
533            TransactionOrSpan::Span(span) => span.set_status(status),
534        }
535    }
536
537    /// Set the operation for this Transaction/Span.
538    pub fn set_op(&self, op: &str) {
539        match self {
540            TransactionOrSpan::Transaction(transaction) => transaction.set_op(op),
541            TransactionOrSpan::Span(span) => span.set_op(op),
542        }
543    }
544
545    /// Set the name (description) for this Transaction/Span.
546    pub fn set_name(&self, name: &str) {
547        match self {
548            TransactionOrSpan::Transaction(transaction) => transaction.set_name(name),
549            TransactionOrSpan::Span(span) => span.set_name(name),
550        }
551    }
552
553    /// Set the HTTP request information for this Transaction/Span.
554    pub fn set_request(&self, request: protocol::Request) {
555        match self {
556            TransactionOrSpan::Transaction(transaction) => transaction.set_request(request),
557            TransactionOrSpan::Span(span) => span.set_request(request),
558        }
559    }
560
561    /// Returns the headers needed for distributed tracing.
562    /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active
563    /// trace's distributed tracing headers.
564    pub fn iter_headers(&self) -> TraceHeadersIter {
565        match self {
566            TransactionOrSpan::Transaction(transaction) => transaction.iter_headers(),
567            TransactionOrSpan::Span(span) => span.iter_headers(),
568        }
569    }
570
571    /// Get the sampling decision for this Transaction/Span.
572    ///
573    /// The returned `bool` does not fully represent the sampling state of this
574    /// Transaction/Span. Although `true` reliably indicates that the
575    /// Transaction/Span is sampled, a value of `false` can mean either that the
576    /// Transaction/Span is not sampled, or that tracing is disabled and the
577    /// sampling decision is deferred. This method therefore should no longer be
578    /// used, especially not for trace continuation purposes.
579    ///
580    /// For trace propagation, use [`Self::iter_headers`] or
581    /// [`crate::Scope::iter_trace_propagation_headers`] instead, to ensure
582    /// correct results.
583    #[deprecated = "the returned value may not accurately represent the sampling decision"]
584    pub fn is_sampled(&self) -> bool {
585        match self {
586            TransactionOrSpan::Transaction(transaction) =>
587            {
588                #[expect(deprecated)]
589                transaction.is_sampled()
590            }
591            TransactionOrSpan::Span(span) =>
592            {
593                #[expect(deprecated)]
594                span.is_sampled()
595            }
596        }
597    }
598
599    /// Starts a new child Span with the given `op` and `description`.
600    ///
601    /// The span must be explicitly finished via [`Span::finish`], as it will
602    /// otherwise not be sent to Sentry.
603    #[must_use = "a span must be explicitly closed via `finish()`"]
604    pub fn start_child(&self, op: &str, description: &str) -> Span {
605        match self {
606            TransactionOrSpan::Transaction(transaction) => transaction.start_child(op, description),
607            TransactionOrSpan::Span(span) => span.start_child(op, description),
608        }
609    }
610
611    /// Starts a new child Span with the given `op`, `description` and `id`.
612    ///
613    /// The span must be explicitly finished via [`Span::finish`], as it will
614    /// otherwise not be sent to Sentry.
615    #[must_use = "a span must be explicitly closed via `finish()`"]
616    pub fn start_child_with_details(
617        &self,
618        op: &str,
619        description: &str,
620        id: SpanId,
621        timestamp: SystemTime,
622    ) -> Span {
623        match self {
624            TransactionOrSpan::Transaction(transaction) => {
625                transaction.start_child_with_details(op, description, id, timestamp)
626            }
627            TransactionOrSpan::Span(span) => {
628                span.start_child_with_details(op, description, id, timestamp)
629            }
630        }
631    }
632
633    #[cfg(feature = "client")]
634    pub(crate) fn apply_to_event(&self, event: &mut protocol::Event<'_>) {
635        if event.contexts.contains_key("trace") {
636            return;
637        }
638
639        let context = match self {
640            TransactionOrSpan::Transaction(transaction) => {
641                transaction.inner.lock().unwrap().context.clone()
642            }
643            TransactionOrSpan::Span(span) => {
644                let span = span.span.lock().unwrap();
645                protocol::TraceContext {
646                    span_id: span.span_id,
647                    trace_id: span.trace_id,
648                    ..Default::default()
649                }
650            }
651        };
652        event.contexts.insert("trace".into(), context.into());
653    }
654
655    /// Finishes the Transaction/Span with the provided end timestamp.
656    ///
657    /// This records the end timestamp and either sends the inner [`Transaction`]
658    /// directly to Sentry, or adds the [`Span`] to its transaction.
659    pub fn finish_with_timestamp(self, timestamp: SystemTime) {
660        match self {
661            TransactionOrSpan::Transaction(transaction) => {
662                transaction.finish_with_timestamp(timestamp)
663            }
664            TransactionOrSpan::Span(span) => span.finish_with_timestamp(timestamp),
665        }
666    }
667
668    /// Finishes the Transaction/Span.
669    ///
670    /// This records the current timestamp as the end timestamp and either sends the inner [`Transaction`]
671    /// directly to Sentry, or adds the [`Span`] to its transaction.
672    pub fn finish(self) {
673        match self {
674            TransactionOrSpan::Transaction(transaction) => transaction.finish(),
675            TransactionOrSpan::Span(span) => span.finish(),
676        }
677    }
678}
679
680#[derive(Debug)]
681pub(crate) struct TransactionInner {
682    #[cfg(feature = "client")]
683    client: Option<Arc<Client>>,
684    tracing_state: TracingState,
685    pub(crate) context: protocol::TraceContext,
686    pub(crate) transaction: Option<protocol::Transaction<'static>>,
687}
688
689type TransactionArc = Arc<Mutex<TransactionInner>>;
690
691/// Functional implementation of how a new transaction's sample rate is chosen.
692///
693/// Returns `None` when tracing is disabled.
694#[cfg(feature = "client")]
695fn transaction_sample_rate(
696    traces_sampling_strategy: &TracesSamplingStrategy,
697    ctx: &TransactionContext,
698) -> Option<f32> {
699    match traces_sampling_strategy {
700        &TracesSamplingStrategy::FixedRate(rate) => Some(ctx.sampled.map_or(rate, f32::from)),
701        TracesSamplingStrategy::Function(traces_sampler) => Some(traces_sampler(ctx)),
702        TracesSamplingStrategy::Disabled => None,
703    }
704}
705
706#[cfg(feature = "client")]
707fn should_continue_trace(
708    incoming: Option<OrganizationId>,
709    sdk: Option<OrganizationId>,
710    strict: bool,
711) -> bool {
712    match (incoming, sdk) {
713        (Some(incoming), Some(sdk)) => incoming == sdk,
714        (Some(_), None) | (None, Some(_)) => !strict,
715        (None, None) => true,
716    }
717}
718
719/// Determine whether the new transaction should be sampled.
720#[cfg(feature = "client")]
721impl Client {
722    /// Determines the [`TracingState`] based on the provided [`TransactionContext`].
723    ///
724    /// This function performs random sampling according to the appropriate sample rate as needed.
725    fn determine_tracing_state(&self, ctx: &TransactionContext) -> TracingState {
726        let client_options = self.options();
727        match transaction_sample_rate(&client_options.traces_sampling_strategy, ctx) {
728            // A return value of Some(_) indicates tracing is enabled.
729            Some(sample_rate) => {
730                let sampled = self.sample_should_send(sample_rate);
731                TracingState::new_enabled(sampled, sample_rate)
732            }
733            // A return value of None indicates tracing is disabled.
734            None => TracingState::new_disabled(ctx.sampled),
735        }
736    }
737}
738
739/// A running Performance Monitoring Transaction.
740///
741/// The transaction needs to be explicitly finished via [`Transaction::finish`],
742/// otherwise neither the transaction nor any of its child spans will be sent
743/// to Sentry.
744#[derive(Clone, Debug)]
745pub struct Transaction {
746    pub(crate) inner: TransactionArc,
747}
748
749/// Iterable for a transaction's [data attributes](protocol::TraceContext::data).
750pub struct TransactionData<'a>(MutexGuard<'a, TransactionInner>);
751
752impl<'a> TransactionData<'a> {
753    /// Iterate over the [data attributes](protocol::TraceContext::data)
754    /// associated with this [transaction][protocol::Transaction].
755    ///
756    /// If the transaction is not sampled for sending,
757    /// the metadata will not be populated at all,
758    /// so the produced iterator is empty.
759    pub fn iter(&self) -> Box<dyn Iterator<Item = (&String, &protocol::Value)> + '_> {
760        if self.0.transaction.is_some() {
761            Box::new(self.0.context.data.iter())
762        } else {
763            Box::new(std::iter::empty())
764        }
765    }
766
767    /// Set a data attribute to be sent with this Transaction.
768    pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) {
769        if self.0.transaction.is_some() {
770            self.0.context.data.insert(key.into(), value);
771        }
772    }
773
774    /// Set a tag to be sent with this Transaction.
775    pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) {
776        if let Some(transaction) = self.0.transaction.as_mut() {
777            transaction.tags.insert(key.into(), value);
778        }
779    }
780}
781
782impl Transaction {
783    #[cfg(feature = "client")]
784    fn new(client: Option<Arc<Client>>, mut ctx: TransactionContext) -> Self {
785        let (tracing_state, transaction) = match client.as_ref() {
786            Some(client) => {
787                let options = client.options();
788                let sdk_org_id = options.org_id.or_else(|| options.dsn.as_ref()?.org_id());
789
790                if ctx.incoming_trace.is_some_and(
791                    |IncomingTrace {
792                         org_id: incoming_org_id,
793                     }| {
794                        !should_continue_trace(
795                            incoming_org_id,
796                            sdk_org_id,
797                            options.strict_trace_continuation,
798                        )
799                    },
800                ) {
801                    ctx.reject_incoming_trace();
802                }
803
804                (
805                    client.determine_tracing_state(&ctx),
806                    Some(protocol::Transaction {
807                        name: Some(ctx.name),
808                        ..Default::default()
809                    }),
810                )
811            }
812            None => (TracingState::new_disabled(ctx.sampled), None),
813        };
814
815        let context = protocol::TraceContext {
816            trace_id: ctx.trace_id,
817            parent_span_id: ctx.parent_span_id,
818            span_id: ctx.span_id,
819            op: Some(ctx.op),
820            ..Default::default()
821        };
822
823        Self {
824            inner: Arc::new(Mutex::new(TransactionInner {
825                client,
826                tracing_state,
827                context,
828                transaction,
829            })),
830        }
831    }
832
833    #[cfg(not(feature = "client"))]
834    fn new_noop(ctx: TransactionContext) -> Self {
835        let context = protocol::TraceContext {
836            trace_id: ctx.trace_id,
837            parent_span_id: ctx.parent_span_id,
838            op: Some(ctx.op),
839            ..Default::default()
840        };
841        let tracing_state = TracingState::new_disabled(ctx.sampled);
842
843        Self {
844            inner: Arc::new(Mutex::new(TransactionInner {
845                tracing_state,
846                context,
847                transaction: None,
848            })),
849        }
850    }
851
852    /// Set a data attribute to be sent with this Transaction.
853    pub fn set_data(&self, key: &str, value: protocol::Value) {
854        let mut inner = self.inner.lock().unwrap();
855        if inner.transaction.is_some() {
856            inner.context.data.insert(key.into(), value);
857        }
858    }
859
860    /// Set some extra information to be sent with this Transaction.
861    pub fn set_extra(&self, key: &str, value: protocol::Value) {
862        let mut inner = self.inner.lock().unwrap();
863        if let Some(transaction) = inner.transaction.as_mut() {
864            transaction.extra.insert(key.into(), value);
865        }
866    }
867
868    /// Sets a tag to a specific value.
869    pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
870        let mut inner = self.inner.lock().unwrap();
871        if let Some(transaction) = inner.transaction.as_mut() {
872            transaction.tags.insert(key.into(), value.to_string());
873        }
874    }
875
876    /// Returns an iterating accessor to the transaction's
877    /// [data attributes](protocol::TraceContext::data).
878    ///
879    /// # Concurrency
880    /// In order to obtain any kind of reference to the `TraceContext::data` field,
881    /// a `Mutex` needs to be locked. The returned `TransactionData` holds on to this lock
882    /// for as long as it lives. Therefore you must take care not to keep the returned
883    /// `TransactionData` around too long or it will never relinquish the lock and you may run into
884    /// a deadlock.
885    pub fn data(&self) -> TransactionData<'_> {
886        TransactionData(self.inner.lock().unwrap())
887    }
888
889    /// Get the TransactionContext of the Transaction.
890    ///
891    /// Note that this clones the underlying value.
892    pub fn get_trace_context(&self) -> protocol::TraceContext {
893        let inner = self.inner.lock().unwrap();
894        inner.context.clone()
895    }
896
897    /// Get the status of the Transaction.
898    pub fn get_status(&self) -> Option<protocol::SpanStatus> {
899        let inner = self.inner.lock().unwrap();
900        inner.context.status
901    }
902
903    /// Set the status of the Transaction.
904    pub fn set_status(&self, status: protocol::SpanStatus) {
905        let mut inner = self.inner.lock().unwrap();
906        inner.context.status = Some(status);
907    }
908
909    /// Set the operation of the Transaction.
910    pub fn set_op(&self, op: &str) {
911        let mut inner = self.inner.lock().unwrap();
912        inner.context.op = Some(op.to_string());
913    }
914
915    /// Set the name of the Transaction.
916    pub fn set_name(&self, name: &str) {
917        let mut inner = self.inner.lock().unwrap();
918        if let Some(transaction) = inner.transaction.as_mut() {
919            transaction.name = Some(name.to_string());
920        }
921    }
922
923    /// Set the HTTP request information for this Transaction.
924    pub fn set_request(&self, request: protocol::Request) {
925        let mut inner = self.inner.lock().unwrap();
926        if let Some(transaction) = inner.transaction.as_mut() {
927            transaction.request = Some(request);
928        }
929    }
930
931    /// Sets the origin for this transaction, indicating what created it.
932    pub fn set_origin(&self, origin: &str) {
933        let mut inner = self.inner.lock().unwrap();
934        inner.context.origin = Some(origin.to_owned());
935    }
936
937    /// Returns the headers needed for distributed tracing.
938    /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active
939    /// trace's distributed tracing headers.
940    pub fn iter_headers(&self) -> TraceHeadersIter {
941        let inner = self.inner.lock().unwrap();
942        let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
943            .with_maybe_sampled(inner.tracing_state.trace_sampled());
944        TraceHeadersIter {
945            sentry_trace: Some(trace.sentry_trace_header()),
946        }
947    }
948
949    /// Get the sampling decision for this Transaction.
950    ///
951    /// The returned `bool` does not fully represent the Transaction's sampling
952    /// state. Although `true` reliably indicates that the Transaction is
953    /// sampled, a value of `false` can mean either that the Transaction is not
954    /// sampled, or that tracing is disabled and the sampling decision is
955    /// deferred. This method therefore should no longer be used, especially not
956    /// for trace continuation purposes.
957    ///
958    /// For trace propagation, use [`Self::iter_headers`] or
959    /// [`crate::Scope::iter_trace_propagation_headers`] instead, to ensure
960    /// correct results.
961    #[deprecated = "the returned value may not accurately represent the sampling decision"]
962    pub fn is_sampled(&self) -> bool {
963        // Checking that we have a `Send` finish action should at least roughly match the old
964        // behavior of this function: we only return true for sampled spans when tracing is
965        // enabled, and false otherwise.
966        #[cfg(feature = "client")]
967        {
968            matches!(
969                self.inner.lock().unwrap().tracing_state.finish_action(),
970                FinishAction::Send { .. }
971            )
972        }
973
974        #[cfg(not(feature = "client"))]
975        false
976    }
977
978    /// Finishes the Transaction with the provided end timestamp.
979    ///
980    /// This records the end timestamp and sends the transaction together with
981    /// all finished child spans to Sentry.
982    pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
983        with_client_impl! {{
984            let mut inner = self.inner.lock().unwrap();
985
986            if let (Some(mut transaction), Some(client)) = (inner.transaction.take(), inner.client.take()) {
987                match inner.tracing_state.finish_action() {
988                    FinishAction::Send { sample_rate } => {
989                        transaction.finish_with_timestamp(_timestamp);
990                        transaction
991                            .contexts
992                            .insert("trace".into(), inner.context.clone().into());
993
994                        Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction));
995                        let opts = client.options();
996                        transaction.release.clone_from(&opts.release);
997                        transaction.environment.clone_from(&opts.environment);
998                        transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone()));
999                        transaction.server_name.clone_from(&opts.server_name);
1000
1001                        let mut dsc = protocol::DynamicSamplingContext::new()
1002                            .with_trace_id(inner.context.trace_id)
1003                            .with_sample_rate(sample_rate)
1004                            .with_sampled(true);
1005                        if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) {
1006                            dsc = dsc.with_public_key(public_key.to_owned());
1007                        }
1008
1009                        drop(inner);
1010
1011                        let mut envelope = protocol::Envelope::new().with_headers(
1012                            protocol::EnvelopeHeaders::new().with_trace(dsc)
1013                        );
1014                        envelope.add_item(transaction);
1015
1016                        client.send_envelope(envelope);
1017                    },
1018                    FinishAction::Discard => {
1019                        client.record_lost_data(&transaction, ClientReportReason::SampleRate);
1020                    },
1021                    FinishAction::Ignore => (),
1022                }
1023            }
1024        }}
1025    }
1026
1027    /// Finishes the Transaction.
1028    ///
1029    /// This records the current timestamp as the end timestamp and sends the transaction together with
1030    /// all finished child spans to Sentry.
1031    pub fn finish(self) {
1032        self.finish_with_timestamp(SystemTime::now());
1033    }
1034
1035    /// Starts a new child Span with the given `op` and `description`.
1036    ///
1037    /// The span must be explicitly finished via [`Span::finish`].
1038    #[must_use = "a span must be explicitly closed via `finish()`"]
1039    pub fn start_child(&self, op: &str, description: &str) -> Span {
1040        let inner = self.inner.lock().unwrap();
1041        let span = protocol::Span {
1042            trace_id: inner.context.trace_id,
1043            parent_span_id: Some(inner.context.span_id),
1044            op: Some(op.into()),
1045            description: if description.is_empty() {
1046                None
1047            } else {
1048                Some(description.into())
1049            },
1050            ..Default::default()
1051        };
1052        Span {
1053            transaction: Arc::clone(&self.inner),
1054            tracing_state: inner.tracing_state,
1055            span: Arc::new(Mutex::new(span)),
1056        }
1057    }
1058
1059    /// Starts a new child Span with the given `op` and `description`.
1060    ///
1061    /// The span must be explicitly finished via [`Span::finish`].
1062    #[must_use = "a span must be explicitly closed via `finish()`"]
1063    pub fn start_child_with_details(
1064        &self,
1065        op: &str,
1066        description: &str,
1067        id: SpanId,
1068        timestamp: SystemTime,
1069    ) -> Span {
1070        let inner = self.inner.lock().unwrap();
1071        let span = protocol::Span {
1072            trace_id: inner.context.trace_id,
1073            parent_span_id: Some(inner.context.span_id),
1074            op: Some(op.into()),
1075            description: if description.is_empty() {
1076                None
1077            } else {
1078                Some(description.into())
1079            },
1080            span_id: id,
1081            start_timestamp: timestamp,
1082            ..Default::default()
1083        };
1084        Span {
1085            transaction: Arc::clone(&self.inner),
1086            tracing_state: inner.tracing_state,
1087            span: Arc::new(Mutex::new(span)),
1088        }
1089    }
1090}
1091
1092impl PartialEq for Transaction {
1093    fn eq(&self, other: &Self) -> bool {
1094        Arc::ptr_eq(&self.inner, &other.inner)
1095    }
1096}
1097
1098/// A smart pointer to a span's [`data` field](protocol::Span::data).
1099pub struct Data<'a>(MutexGuard<'a, protocol::Span>);
1100
1101impl Data<'_> {
1102    /// Set some extra information to be sent with this Span.
1103    pub fn set_data(&mut self, key: String, value: protocol::Value) {
1104        self.0.data.insert(key, value);
1105    }
1106
1107    /// Set some tag to be sent with this Span.
1108    pub fn set_tag(&mut self, key: String, value: String) {
1109        self.0.tags.insert(key, value);
1110    }
1111}
1112
1113impl Deref for Data<'_> {
1114    type Target = BTreeMap<String, protocol::Value>;
1115
1116    fn deref(&self) -> &Self::Target {
1117        &self.0.data
1118    }
1119}
1120
1121impl DerefMut for Data<'_> {
1122    fn deref_mut(&mut self) -> &mut Self::Target {
1123        &mut self.0.data
1124    }
1125}
1126
1127/// A running Performance Monitoring Span.
1128///
1129/// The span needs to be explicitly finished via [`Span::finish`], otherwise it
1130/// will not be sent to Sentry.
1131#[derive(Clone, Debug)]
1132pub struct Span {
1133    pub(crate) transaction: TransactionArc,
1134    tracing_state: TracingState,
1135    span: SpanArc,
1136}
1137
1138type SpanArc = Arc<Mutex<protocol::Span>>;
1139
1140impl Span {
1141    /// Set some extra information to be sent with this Transaction.
1142    pub fn set_data(&self, key: &str, value: protocol::Value) {
1143        let mut span = self.span.lock().unwrap();
1144        span.data.insert(key.into(), value);
1145    }
1146
1147    /// Sets a tag to a specific value.
1148    pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
1149        let mut span = self.span.lock().unwrap();
1150        span.tags.insert(key.into(), value.to_string());
1151    }
1152
1153    /// Returns a smart pointer to the span's [`data` field](protocol::Span::data).
1154    ///
1155    /// Since [`Data`] implements `Deref` and `DerefMut`, this can be used to read and mutate
1156    /// the span data.
1157    ///
1158    /// # Concurrency
1159    /// In order to obtain any kind of reference to the `data` field,
1160    /// a `Mutex` needs to be locked. The returned `Data` holds on to this lock
1161    /// for as long as it lives. Therefore you must take care not to keep the returned
1162    /// `Data` around too long or it will never relinquish the lock and you may run into
1163    /// a deadlock.
1164    pub fn data(&self) -> Data<'_> {
1165        Data(self.span.lock().unwrap())
1166    }
1167
1168    /// Get the TransactionContext of the Span.
1169    ///
1170    /// Note that this clones the underlying value.
1171    pub fn get_trace_context(&self) -> protocol::TraceContext {
1172        let transaction = self.transaction.lock().unwrap();
1173        transaction.context.clone()
1174    }
1175
1176    /// Get the current span ID.
1177    pub fn get_span_id(&self) -> protocol::SpanId {
1178        let span = self.span.lock().unwrap();
1179        span.span_id
1180    }
1181
1182    /// Get the status of the Span.
1183    pub fn get_status(&self) -> Option<protocol::SpanStatus> {
1184        let span = self.span.lock().unwrap();
1185        span.status
1186    }
1187
1188    /// Set the status of the Span.
1189    pub fn set_status(&self, status: protocol::SpanStatus) {
1190        let mut span = self.span.lock().unwrap();
1191        span.status = Some(status);
1192    }
1193
1194    /// Set the operation of the Span.
1195    pub fn set_op(&self, op: &str) {
1196        let mut span = self.span.lock().unwrap();
1197        span.op = Some(op.to_string());
1198    }
1199
1200    /// Set the name (description) of the Span.
1201    pub fn set_name(&self, name: &str) {
1202        let mut span = self.span.lock().unwrap();
1203        span.description = Some(name.to_string());
1204    }
1205
1206    /// Set the HTTP request information for this Span.
1207    pub fn set_request(&self, request: protocol::Request) {
1208        let mut span = self.span.lock().unwrap();
1209        // Extract values from the request to be used as data in the span.
1210        if let Some(method) = request.method {
1211            span.data.insert("method".into(), method.into());
1212        }
1213        if let Some(url) = request.url {
1214            span.data.insert("url".into(), url.to_string().into());
1215        }
1216        if let Some(data) = request.data {
1217            if let Ok(data) = serde_json::from_str::<serde_json::Value>(&data) {
1218                span.data.insert("data".into(), data);
1219            } else {
1220                span.data.insert("data".into(), data.into());
1221            }
1222        }
1223        if let Some(query_string) = request.query_string {
1224            span.data.insert("query_string".into(), query_string.into());
1225        }
1226        if let Some(cookies) = request.cookies {
1227            span.data.insert("cookies".into(), cookies.into());
1228        }
1229        if !request.headers.is_empty() {
1230            if let Ok(headers) = serde_json::to_value(request.headers) {
1231                span.data.insert("headers".into(), headers);
1232            }
1233        }
1234        if !request.env.is_empty() {
1235            if let Ok(env) = serde_json::to_value(request.env) {
1236                span.data.insert("env".into(), env);
1237            }
1238        }
1239    }
1240
1241    /// Returns the headers needed for distributed tracing.
1242    /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active
1243    /// trace's distributed tracing headers.
1244    pub fn iter_headers(&self) -> TraceHeadersIter {
1245        let span = self.span.lock().unwrap();
1246        let trace = TracePropagationContext::new(span.trace_id, span.span_id)
1247            .with_maybe_sampled(self.tracing_state.trace_sampled());
1248
1249        TraceHeadersIter {
1250            sentry_trace: Some(trace.sentry_trace_header()),
1251        }
1252    }
1253
1254    /// Get the sampling decision for this Span.
1255    ///
1256    /// The returned `bool` does not fully represent the Span's sampling state.
1257    /// Although `true` reliably indicates that the Span is sampled, a value of
1258    /// `false` can mean either that the Span is not sampled, or that tracing is
1259    /// disabled and the sampling decision is deferred. This method therefore
1260    /// should no longer be used, especially not for trace continuation purposes.
1261    ///
1262    /// For trace propagation, use [`Self::iter_headers`] or
1263    /// [`crate::Scope::iter_trace_propagation_headers`] instead, to ensure
1264    /// correct results.
1265    #[deprecated = "the returned value may not accurately represent the sampling decision"]
1266    pub fn is_sampled(&self) -> bool {
1267        // Checking that we have a `Send` finish action should at least roughly match the old
1268        // behavior of this function: we only return true for sampled spans when tracing is
1269        // enabled, and false otherwise.
1270        #[cfg(feature = "client")]
1271        {
1272            matches!(
1273                self.tracing_state.finish_action(),
1274                FinishAction::Send { .. }
1275            )
1276        }
1277
1278        #[cfg(not(feature = "client"))]
1279        false
1280    }
1281
1282    /// Finishes the Span with the provided end timestamp.
1283    ///
1284    /// This will record the end timestamp and add the span to the transaction
1285    /// in which it was started.
1286    pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
1287        with_client_impl! {{
1288            let mut span = self.span.lock().unwrap();
1289            if span.timestamp.is_some() {
1290                // the span was already finished
1291                return;
1292            }
1293            span.finish_with_timestamp(_timestamp);
1294            let mut inner = self.transaction.lock().unwrap();
1295            // Disabled traces do not retain finished spans or report span losses.
1296            if matches!(inner.tracing_state.finish_action(), FinishAction::Ignore) {
1297                return;
1298            }
1299            if let Some(transaction) = inner.transaction.as_mut() {
1300                if transaction.spans.len() <= MAX_SPANS {
1301                    transaction.spans.push(span.clone());
1302                } else if let Some(client) = inner.client.as_ref() {
1303                    client.record_lost_data(&*span, ClientReportReason::BufferOverflow);
1304                }
1305            }
1306        }}
1307    }
1308
1309    /// Finishes the Span.
1310    ///
1311    /// This will record the current timestamp as the end timestamp and add the span to the
1312    /// transaction in which it was started.
1313    pub fn finish(self) {
1314        self.finish_with_timestamp(SystemTime::now());
1315    }
1316
1317    /// Starts a new child Span with the given `op` and `description`.
1318    ///
1319    /// The span must be explicitly finished via [`Span::finish`].
1320    #[must_use = "a span must be explicitly closed via `finish()`"]
1321    pub fn start_child(&self, op: &str, description: &str) -> Span {
1322        let span = self.span.lock().unwrap();
1323        let span = protocol::Span {
1324            trace_id: span.trace_id,
1325            parent_span_id: Some(span.span_id),
1326            op: Some(op.into()),
1327            description: if description.is_empty() {
1328                None
1329            } else {
1330                Some(description.into())
1331            },
1332            ..Default::default()
1333        };
1334        Span {
1335            transaction: self.transaction.clone(),
1336            tracing_state: self.tracing_state,
1337            span: Arc::new(Mutex::new(span)),
1338        }
1339    }
1340
1341    /// Starts a new child Span with the given `op` and `description`.
1342    ///
1343    /// The span must be explicitly finished via [`Span::finish`].
1344    #[must_use = "a span must be explicitly closed via `finish()`"]
1345    fn start_child_with_details(
1346        &self,
1347        op: &str,
1348        description: &str,
1349        id: SpanId,
1350        timestamp: SystemTime,
1351    ) -> Span {
1352        let span = self.span.lock().unwrap();
1353        let span = protocol::Span {
1354            trace_id: span.trace_id,
1355            parent_span_id: Some(span.span_id),
1356            op: Some(op.into()),
1357            description: if description.is_empty() {
1358                None
1359            } else {
1360                Some(description.into())
1361            },
1362            span_id: id,
1363            start_timestamp: timestamp,
1364            ..Default::default()
1365        };
1366        Span {
1367            transaction: self.transaction.clone(),
1368            tracing_state: self.tracing_state,
1369            span: Arc::new(Mutex::new(span)),
1370        }
1371    }
1372}
1373
1374impl PartialEq for Span {
1375    fn eq(&self, other: &Self) -> bool {
1376        Arc::ptr_eq(&self.span, &other.span)
1377    }
1378}
1379
1380/// Represents a key-value pair such as an HTTP header.
1381pub type TraceHeader = (&'static str, String);
1382
1383/// An Iterator over HTTP header names and values needed for distributed tracing.
1384///
1385/// This currently only yields the `sentry-trace` header, but other headers
1386/// may be added in the future.
1387pub struct TraceHeadersIter {
1388    sentry_trace: Option<String>,
1389}
1390
1391impl TraceHeadersIter {
1392    #[cfg(feature = "client")]
1393    pub(crate) fn new(sentry_trace: String) -> Self {
1394        Self {
1395            sentry_trace: Some(sentry_trace),
1396        }
1397    }
1398}
1399
1400impl Iterator for TraceHeadersIter {
1401    type Item = (&'static str, String);
1402
1403    fn next(&mut self) -> Option<Self::Item> {
1404        self.sentry_trace.take().map(|st| ("sentry-trace", st))
1405    }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410    use std::sync::Arc;
1411
1412    use super::*;
1413
1414    #[test]
1415    fn disabled_forwards_trace_id() {
1416        let headers = [(
1417            "SenTrY-TRAce",
1418            "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1419        )];
1420        let ctx = TransactionContext::continue_from_headers("noop", "noop", headers);
1421        let trx = start_transaction(ctx);
1422
1423        let span = trx.start_child("noop", "noop");
1424
1425        let header = span.iter_headers().next().unwrap().1;
1426        let parsed =
1427            TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();
1428
1429        assert_eq!(
1430            &parsed.trace_id.to_string(),
1431            "09e04486820349518ac7b5d2adbf6ba5"
1432        );
1433        assert_eq!(parsed.sampled, Some(true));
1434    }
1435
1436    #[test]
1437    fn transaction_context_public_getters() {
1438        let mut ctx = TransactionContext::new("test-name", "test-operation");
1439        assert_eq!(ctx.name(), "test-name");
1440        assert_eq!(ctx.operation(), "test-operation");
1441        assert_eq!(ctx.sampled(), None);
1442
1443        ctx.set_sampled(true);
1444        assert_eq!(ctx.sampled(), Some(true));
1445    }
1446
1447    #[test]
1448    fn continue_from_headers_stores_incoming_org_id() {
1449        let ctx = TransactionContext::continue_from_headers(
1450            "noop",
1451            "noop",
1452            [
1453                (
1454                    "sentry-trace",
1455                    "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1456                ),
1457                ("baggage", "sentry-org_id=123"),
1458            ],
1459        );
1460
1461        assert_eq!(
1462            ctx.incoming_trace.map(|incoming| incoming.org_id),
1463            Some(Some("123".parse().unwrap()))
1464        );
1465    }
1466
1467    #[test]
1468    fn continue_from_headers_does_not_keep_org_id_without_sentry_trace() {
1469        let ctx = TransactionContext::continue_from_headers(
1470            "noop",
1471            "noop",
1472            [("baggage", "sentry-org_id=123")],
1473        );
1474
1475        assert!(ctx.incoming_trace.is_none());
1476        assert_eq!(ctx.parent_span_id, None);
1477    }
1478
1479    #[cfg(feature = "client")]
1480    #[test]
1481    fn compute_transaction_sample_rate() {
1482        let ctx = TransactionContext::new("noop", "noop");
1483        assert_eq!(
1484            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1485            Some(0.3)
1486        );
1487        assert_eq!(
1488            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.7), &ctx),
1489            Some(0.7)
1490        );
1491
1492        let mut ctx = TransactionContext::new("noop", "noop");
1493        ctx.set_sampled(true);
1494        assert_eq!(
1495            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1496            Some(1.0)
1497        );
1498        ctx.set_sampled(false);
1499        assert_eq!(
1500            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1501            Some(0.0)
1502        );
1503
1504        let ctx = TransactionContext::new("noop", "noop");
1505        assert_eq!(
1506            transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1507            None
1508        );
1509        let mut ctx = TransactionContext::new("noop", "noop");
1510        ctx.set_sampled(true);
1511        assert_eq!(
1512            transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1513            None
1514        );
1515        ctx.set_sampled(false);
1516        assert_eq!(
1517            transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1518            None
1519        );
1520
1521        // Function and FixedRate are mutually exclusive strategy variants. A function
1522        // strategy can ignore parent sampling or choose to inspect it.
1523        let mut ctx = TransactionContext::new("noop", "noop");
1524        let sampler = |_: &TransactionContext| 0.7_f32;
1525        let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1526        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1527        ctx.set_sampled(false);
1528        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1529
1530        let sampler = |ctx: &TransactionContext| match ctx.sampled() {
1531            Some(true) => 0.8_f32,
1532            Some(false) => 0.4_f32,
1533            None => 0.6_f32,
1534        };
1535        let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1536        ctx.set_sampled(true);
1537        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.8));
1538        ctx.set_sampled(None);
1539        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.6));
1540
1541        let sampler = |ctx: &TransactionContext| {
1542            if ctx.name() == "must-name" || ctx.operation() == "must-operation" {
1543                return 1.0;
1544            }
1545
1546            if let Some(custom) = ctx.custom() {
1547                if let Some(rate) = custom.get("rate") {
1548                    if let Some(rate) = rate.as_f64() {
1549                        return rate as f32;
1550                    }
1551                }
1552            }
1553
1554            0.1
1555        };
1556        let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1557        let ctx = TransactionContext::new("noop", "must-operation");
1558        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1559        let ctx = TransactionContext::new("must-name", "noop");
1560        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1561        let mut ctx = TransactionContext::new("noop", "noop");
1562        ctx.custom_insert("rate".to_owned(), serde_json::json!(0.7));
1563        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1564    }
1565}