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 no client was bound when the transaction was started,
757    /// the produced iterator is empty.
758    pub fn iter(&self) -> Box<dyn Iterator<Item = (&String, &protocol::Value)> + '_> {
759        if self.0.transaction.is_some() {
760            Box::new(self.0.context.data.iter())
761        } else {
762            Box::new(std::iter::empty())
763        }
764    }
765
766    /// Set a data attribute to be sent with this Transaction.
767    pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) {
768        if self.0.transaction.is_some() {
769            self.0.context.data.insert(key.into(), value);
770        }
771    }
772
773    /// Set a tag to be sent with this Transaction.
774    pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) {
775        if let Some(transaction) = self.0.transaction.as_mut() {
776            transaction.tags.insert(key.into(), value);
777        }
778    }
779}
780
781impl Transaction {
782    #[cfg(feature = "client")]
783    fn new(client: Option<Arc<Client>>, mut ctx: TransactionContext) -> Self {
784        let (tracing_state, transaction) = match client.as_ref() {
785            Some(client) => {
786                let options = client.options();
787                let sdk_org_id = options.org_id.or_else(|| options.dsn.as_ref()?.org_id());
788
789                if ctx.incoming_trace.is_some_and(
790                    |IncomingTrace {
791                         org_id: incoming_org_id,
792                     }| {
793                        !should_continue_trace(
794                            incoming_org_id,
795                            sdk_org_id,
796                            options.strict_trace_continuation,
797                        )
798                    },
799                ) {
800                    ctx.reject_incoming_trace();
801                }
802
803                (
804                    client.determine_tracing_state(&ctx),
805                    Some(protocol::Transaction {
806                        name: Some(ctx.name),
807                        ..Default::default()
808                    }),
809                )
810            }
811            None => (TracingState::new_disabled(ctx.sampled), None),
812        };
813
814        let context = protocol::TraceContext {
815            trace_id: ctx.trace_id,
816            parent_span_id: ctx.parent_span_id,
817            span_id: ctx.span_id,
818            op: Some(ctx.op),
819            ..Default::default()
820        };
821
822        Self {
823            inner: Arc::new(Mutex::new(TransactionInner {
824                client,
825                tracing_state,
826                context,
827                transaction,
828            })),
829        }
830    }
831
832    #[cfg(not(feature = "client"))]
833    fn new_noop(ctx: TransactionContext) -> Self {
834        let context = protocol::TraceContext {
835            trace_id: ctx.trace_id,
836            parent_span_id: ctx.parent_span_id,
837            op: Some(ctx.op),
838            ..Default::default()
839        };
840        let tracing_state = TracingState::new_disabled(ctx.sampled);
841
842        Self {
843            inner: Arc::new(Mutex::new(TransactionInner {
844                tracing_state,
845                context,
846                transaction: None,
847            })),
848        }
849    }
850
851    /// Set a data attribute to be sent with this Transaction.
852    pub fn set_data(&self, key: &str, value: protocol::Value) {
853        let mut inner = self.inner.lock().unwrap();
854        if inner.transaction.is_some() {
855            inner.context.data.insert(key.into(), value);
856        }
857    }
858
859    /// Set some extra information to be sent with this Transaction.
860    pub fn set_extra(&self, key: &str, value: protocol::Value) {
861        let mut inner = self.inner.lock().unwrap();
862        if let Some(transaction) = inner.transaction.as_mut() {
863            transaction.extra.insert(key.into(), value);
864        }
865    }
866
867    /// Sets a tag to a specific value.
868    pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
869        let mut inner = self.inner.lock().unwrap();
870        if let Some(transaction) = inner.transaction.as_mut() {
871            transaction.tags.insert(key.into(), value.to_string());
872        }
873    }
874
875    /// Returns an iterating accessor to the transaction's
876    /// [data attributes](protocol::TraceContext::data).
877    ///
878    /// # Concurrency
879    /// In order to obtain any kind of reference to the `TraceContext::data` field,
880    /// a `Mutex` needs to be locked. The returned `TransactionData` holds on to this lock
881    /// for as long as it lives. Therefore you must take care not to keep the returned
882    /// `TransactionData` around too long or it will never relinquish the lock and you may run into
883    /// a deadlock.
884    pub fn data(&self) -> TransactionData<'_> {
885        TransactionData(self.inner.lock().unwrap())
886    }
887
888    /// Get the TransactionContext of the Transaction.
889    ///
890    /// Note that this clones the underlying value.
891    pub fn get_trace_context(&self) -> protocol::TraceContext {
892        let inner = self.inner.lock().unwrap();
893        inner.context.clone()
894    }
895
896    /// Get the status of the Transaction.
897    pub fn get_status(&self) -> Option<protocol::SpanStatus> {
898        let inner = self.inner.lock().unwrap();
899        inner.context.status
900    }
901
902    /// Set the status of the Transaction.
903    pub fn set_status(&self, status: protocol::SpanStatus) {
904        let mut inner = self.inner.lock().unwrap();
905        inner.context.status = Some(status);
906    }
907
908    /// Set the operation of the Transaction.
909    pub fn set_op(&self, op: &str) {
910        let mut inner = self.inner.lock().unwrap();
911        inner.context.op = Some(op.to_string());
912    }
913
914    /// Set the name of the Transaction.
915    pub fn set_name(&self, name: &str) {
916        let mut inner = self.inner.lock().unwrap();
917        if let Some(transaction) = inner.transaction.as_mut() {
918            transaction.name = Some(name.to_string());
919        }
920    }
921
922    /// Set the HTTP request information for this Transaction.
923    pub fn set_request(&self, request: protocol::Request) {
924        let mut inner = self.inner.lock().unwrap();
925        if let Some(transaction) = inner.transaction.as_mut() {
926            transaction.request = Some(request);
927        }
928    }
929
930    /// Sets the origin for this transaction, indicating what created it.
931    pub fn set_origin(&self, origin: &str) {
932        let mut inner = self.inner.lock().unwrap();
933        inner.context.origin = Some(origin.to_owned());
934    }
935
936    /// Returns the headers needed for distributed tracing.
937    /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active
938    /// trace's distributed tracing headers.
939    pub fn iter_headers(&self) -> TraceHeadersIter {
940        let inner = self.inner.lock().unwrap();
941        let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
942            .with_maybe_sampled(inner.tracing_state.trace_sampled());
943        TraceHeadersIter {
944            sentry_trace: Some(trace.sentry_trace_header()),
945        }
946    }
947
948    /// Get the sampling decision for this Transaction.
949    ///
950    /// The returned `bool` does not fully represent the Transaction's sampling
951    /// state. Although `true` reliably indicates that the Transaction is
952    /// sampled, a value of `false` can mean either that the Transaction is not
953    /// sampled, or that tracing is disabled and the sampling decision is
954    /// deferred. This method therefore should no longer be used, especially not
955    /// for trace continuation purposes.
956    ///
957    /// For trace propagation, use [`Self::iter_headers`] or
958    /// [`crate::Scope::iter_trace_propagation_headers`] instead, to ensure
959    /// correct results.
960    #[deprecated = "the returned value may not accurately represent the sampling decision"]
961    pub fn is_sampled(&self) -> bool {
962        // Checking that we have a `Send` finish action should at least roughly match the old
963        // behavior of this function: we only return true for sampled spans when tracing is
964        // enabled, and false otherwise.
965        #[cfg(feature = "client")]
966        {
967            matches!(
968                self.inner.lock().unwrap().tracing_state.finish_action(),
969                FinishAction::Send { .. }
970            )
971        }
972
973        #[cfg(not(feature = "client"))]
974        false
975    }
976
977    /// Finishes the Transaction with the provided end timestamp.
978    ///
979    /// This records the end timestamp and sends the transaction together with
980    /// all finished child spans to Sentry.
981    pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
982        with_client_impl! {{
983            let mut inner = self.inner.lock().unwrap();
984
985            if let (Some(mut transaction), Some(client)) = (inner.transaction.take(), inner.client.take()) {
986                match inner.tracing_state.finish_action() {
987                    FinishAction::Send { sample_rate } => {
988                        transaction.finish_with_timestamp(_timestamp);
989                        transaction
990                            .contexts
991                            .insert("trace".into(), inner.context.clone().into());
992
993                        Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction));
994                        let opts = client.options();
995                        transaction.release.clone_from(&opts.release);
996                        transaction.environment.clone_from(&opts.environment);
997                        transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone()));
998                        transaction.server_name.clone_from(&opts.server_name);
999
1000                        let mut dsc = protocol::DynamicSamplingContext::new()
1001                            .with_trace_id(inner.context.trace_id)
1002                            .with_sample_rate(sample_rate)
1003                            .with_sampled(true);
1004                        if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) {
1005                            dsc = dsc.with_public_key(public_key.to_owned());
1006                        }
1007
1008                        drop(inner);
1009
1010                        let mut envelope = protocol::Envelope::new().with_headers(
1011                            protocol::EnvelopeHeaders::new().with_trace(dsc)
1012                        );
1013                        envelope.add_item(transaction);
1014
1015                        client.send_envelope(envelope);
1016                    },
1017                    FinishAction::Discard => {
1018                        client.record_lost_data(&transaction, ClientReportReason::SampleRate);
1019                    },
1020                    FinishAction::Ignore => (),
1021                }
1022            }
1023        }}
1024    }
1025
1026    /// Finishes the Transaction.
1027    ///
1028    /// This records the current timestamp as the end timestamp and sends the transaction together with
1029    /// all finished child spans to Sentry.
1030    pub fn finish(self) {
1031        self.finish_with_timestamp(SystemTime::now());
1032    }
1033
1034    /// Starts a new child Span with the given `op` and `description`.
1035    ///
1036    /// The span must be explicitly finished via [`Span::finish`].
1037    #[must_use = "a span must be explicitly closed via `finish()`"]
1038    pub fn start_child(&self, op: &str, description: &str) -> Span {
1039        let inner = self.inner.lock().unwrap();
1040        let span = protocol::Span {
1041            trace_id: inner.context.trace_id,
1042            parent_span_id: Some(inner.context.span_id),
1043            op: Some(op.into()),
1044            description: if description.is_empty() {
1045                None
1046            } else {
1047                Some(description.into())
1048            },
1049            ..Default::default()
1050        };
1051        Span {
1052            transaction: Arc::clone(&self.inner),
1053            tracing_state: inner.tracing_state,
1054            span: Arc::new(Mutex::new(span)),
1055        }
1056    }
1057
1058    /// Starts a new child Span with the given `op` and `description`.
1059    ///
1060    /// The span must be explicitly finished via [`Span::finish`].
1061    #[must_use = "a span must be explicitly closed via `finish()`"]
1062    pub fn start_child_with_details(
1063        &self,
1064        op: &str,
1065        description: &str,
1066        id: SpanId,
1067        timestamp: SystemTime,
1068    ) -> Span {
1069        let inner = self.inner.lock().unwrap();
1070        let span = protocol::Span {
1071            trace_id: inner.context.trace_id,
1072            parent_span_id: Some(inner.context.span_id),
1073            op: Some(op.into()),
1074            description: if description.is_empty() {
1075                None
1076            } else {
1077                Some(description.into())
1078            },
1079            span_id: id,
1080            start_timestamp: timestamp,
1081            ..Default::default()
1082        };
1083        Span {
1084            transaction: Arc::clone(&self.inner),
1085            tracing_state: inner.tracing_state,
1086            span: Arc::new(Mutex::new(span)),
1087        }
1088    }
1089}
1090
1091impl PartialEq for Transaction {
1092    fn eq(&self, other: &Self) -> bool {
1093        Arc::ptr_eq(&self.inner, &other.inner)
1094    }
1095}
1096
1097/// A smart pointer to a span's [`data` field](protocol::Span::data).
1098pub struct Data<'a>(MutexGuard<'a, protocol::Span>);
1099
1100impl Data<'_> {
1101    /// Set some extra information to be sent with this Span.
1102    pub fn set_data(&mut self, key: String, value: protocol::Value) {
1103        self.0.data.insert(key, value);
1104    }
1105
1106    /// Set some tag to be sent with this Span.
1107    pub fn set_tag(&mut self, key: String, value: String) {
1108        self.0.tags.insert(key, value);
1109    }
1110}
1111
1112impl Deref for Data<'_> {
1113    type Target = BTreeMap<String, protocol::Value>;
1114
1115    fn deref(&self) -> &Self::Target {
1116        &self.0.data
1117    }
1118}
1119
1120impl DerefMut for Data<'_> {
1121    fn deref_mut(&mut self) -> &mut Self::Target {
1122        &mut self.0.data
1123    }
1124}
1125
1126/// A running Performance Monitoring Span.
1127///
1128/// The span needs to be explicitly finished via [`Span::finish`], otherwise it
1129/// will not be sent to Sentry.
1130#[derive(Clone, Debug)]
1131pub struct Span {
1132    pub(crate) transaction: TransactionArc,
1133    tracing_state: TracingState,
1134    span: SpanArc,
1135}
1136
1137type SpanArc = Arc<Mutex<protocol::Span>>;
1138
1139impl Span {
1140    /// Set some extra information to be sent with this Transaction.
1141    pub fn set_data(&self, key: &str, value: protocol::Value) {
1142        let mut span = self.span.lock().unwrap();
1143        span.data.insert(key.into(), value);
1144    }
1145
1146    /// Sets a tag to a specific value.
1147    pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
1148        let mut span = self.span.lock().unwrap();
1149        span.tags.insert(key.into(), value.to_string());
1150    }
1151
1152    /// Returns a smart pointer to the span's [`data` field](protocol::Span::data).
1153    ///
1154    /// Since [`Data`] implements `Deref` and `DerefMut`, this can be used to read and mutate
1155    /// the span data.
1156    ///
1157    /// # Concurrency
1158    /// In order to obtain any kind of reference to the `data` field,
1159    /// a `Mutex` needs to be locked. The returned `Data` holds on to this lock
1160    /// for as long as it lives. Therefore you must take care not to keep the returned
1161    /// `Data` around too long or it will never relinquish the lock and you may run into
1162    /// a deadlock.
1163    pub fn data(&self) -> Data<'_> {
1164        Data(self.span.lock().unwrap())
1165    }
1166
1167    /// Get the TransactionContext of the Span.
1168    ///
1169    /// Note that this clones the underlying value.
1170    pub fn get_trace_context(&self) -> protocol::TraceContext {
1171        let transaction = self.transaction.lock().unwrap();
1172        transaction.context.clone()
1173    }
1174
1175    /// Get the current span ID.
1176    pub fn get_span_id(&self) -> protocol::SpanId {
1177        let span = self.span.lock().unwrap();
1178        span.span_id
1179    }
1180
1181    /// Get the status of the Span.
1182    pub fn get_status(&self) -> Option<protocol::SpanStatus> {
1183        let span = self.span.lock().unwrap();
1184        span.status
1185    }
1186
1187    /// Set the status of the Span.
1188    pub fn set_status(&self, status: protocol::SpanStatus) {
1189        let mut span = self.span.lock().unwrap();
1190        span.status = Some(status);
1191    }
1192
1193    /// Set the operation of the Span.
1194    pub fn set_op(&self, op: &str) {
1195        let mut span = self.span.lock().unwrap();
1196        span.op = Some(op.to_string());
1197    }
1198
1199    /// Set the name (description) of the Span.
1200    pub fn set_name(&self, name: &str) {
1201        let mut span = self.span.lock().unwrap();
1202        span.description = Some(name.to_string());
1203    }
1204
1205    /// Set the HTTP request information for this Span.
1206    pub fn set_request(&self, request: protocol::Request) {
1207        let mut span = self.span.lock().unwrap();
1208        // Extract values from the request to be used as data in the span.
1209        if let Some(method) = request.method {
1210            span.data.insert("method".into(), method.into());
1211        }
1212        if let Some(url) = request.url {
1213            span.data.insert("url".into(), url.to_string().into());
1214        }
1215        if let Some(data) = request.data {
1216            if let Ok(data) = serde_json::from_str::<serde_json::Value>(&data) {
1217                span.data.insert("data".into(), data);
1218            } else {
1219                span.data.insert("data".into(), data.into());
1220            }
1221        }
1222        if let Some(query_string) = request.query_string {
1223            span.data.insert("query_string".into(), query_string.into());
1224        }
1225        if let Some(cookies) = request.cookies {
1226            span.data.insert("cookies".into(), cookies.into());
1227        }
1228        if !request.headers.is_empty() {
1229            if let Ok(headers) = serde_json::to_value(request.headers) {
1230                span.data.insert("headers".into(), headers);
1231            }
1232        }
1233        if !request.env.is_empty() {
1234            if let Ok(env) = serde_json::to_value(request.env) {
1235                span.data.insert("env".into(), env);
1236            }
1237        }
1238    }
1239
1240    /// Returns the headers needed for distributed tracing.
1241    /// Use [`crate::Scope::iter_trace_propagation_headers`] to obtain the active
1242    /// trace's distributed tracing headers.
1243    pub fn iter_headers(&self) -> TraceHeadersIter {
1244        let span = self.span.lock().unwrap();
1245        let trace = TracePropagationContext::new(span.trace_id, span.span_id)
1246            .with_maybe_sampled(self.tracing_state.trace_sampled());
1247
1248        TraceHeadersIter {
1249            sentry_trace: Some(trace.sentry_trace_header()),
1250        }
1251    }
1252
1253    /// Get the sampling decision for this Span.
1254    ///
1255    /// The returned `bool` does not fully represent the Span's sampling state.
1256    /// Although `true` reliably indicates that the Span is sampled, a value of
1257    /// `false` can mean either that the Span is not sampled, or that tracing is
1258    /// disabled and the sampling decision is deferred. This method therefore
1259    /// should no longer be used, especially not for trace continuation purposes.
1260    ///
1261    /// For trace propagation, use [`Self::iter_headers`] or
1262    /// [`crate::Scope::iter_trace_propagation_headers`] instead, to ensure
1263    /// correct results.
1264    #[deprecated = "the returned value may not accurately represent the sampling decision"]
1265    pub fn is_sampled(&self) -> bool {
1266        // Checking that we have a `Send` finish action should at least roughly match the old
1267        // behavior of this function: we only return true for sampled spans when tracing is
1268        // enabled, and false otherwise.
1269        #[cfg(feature = "client")]
1270        {
1271            matches!(
1272                self.tracing_state.finish_action(),
1273                FinishAction::Send { .. }
1274            )
1275        }
1276
1277        #[cfg(not(feature = "client"))]
1278        false
1279    }
1280
1281    /// Finishes the Span with the provided end timestamp.
1282    ///
1283    /// This will record the end timestamp and add the span to the transaction
1284    /// in which it was started.
1285    pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
1286        with_client_impl! {{
1287            let mut span = self.span.lock().unwrap();
1288            if span.timestamp.is_some() {
1289                // the span was already finished
1290                return;
1291            }
1292            span.finish_with_timestamp(_timestamp);
1293            let mut inner = self.transaction.lock().unwrap();
1294            // Disabled traces do not retain finished spans or report span losses.
1295            if matches!(inner.tracing_state.finish_action(), FinishAction::Ignore) {
1296                return;
1297            }
1298            if let Some(transaction) = inner.transaction.as_mut() {
1299                if transaction.spans.len() <= MAX_SPANS {
1300                    transaction.spans.push(span.clone());
1301                } else if let Some(client) = inner.client.as_ref() {
1302                    client.record_lost_data(&*span, ClientReportReason::BufferOverflow);
1303                }
1304            }
1305        }}
1306    }
1307
1308    /// Finishes the Span.
1309    ///
1310    /// This will record the current timestamp as the end timestamp and add the span to the
1311    /// transaction in which it was started.
1312    pub fn finish(self) {
1313        self.finish_with_timestamp(SystemTime::now());
1314    }
1315
1316    /// Starts a new child Span with the given `op` and `description`.
1317    ///
1318    /// The span must be explicitly finished via [`Span::finish`].
1319    #[must_use = "a span must be explicitly closed via `finish()`"]
1320    pub fn start_child(&self, op: &str, description: &str) -> Span {
1321        let span = self.span.lock().unwrap();
1322        let span = protocol::Span {
1323            trace_id: span.trace_id,
1324            parent_span_id: Some(span.span_id),
1325            op: Some(op.into()),
1326            description: if description.is_empty() {
1327                None
1328            } else {
1329                Some(description.into())
1330            },
1331            ..Default::default()
1332        };
1333        Span {
1334            transaction: self.transaction.clone(),
1335            tracing_state: self.tracing_state,
1336            span: Arc::new(Mutex::new(span)),
1337        }
1338    }
1339
1340    /// Starts a new child Span with the given `op` and `description`.
1341    ///
1342    /// The span must be explicitly finished via [`Span::finish`].
1343    #[must_use = "a span must be explicitly closed via `finish()`"]
1344    fn start_child_with_details(
1345        &self,
1346        op: &str,
1347        description: &str,
1348        id: SpanId,
1349        timestamp: SystemTime,
1350    ) -> Span {
1351        let span = self.span.lock().unwrap();
1352        let span = protocol::Span {
1353            trace_id: span.trace_id,
1354            parent_span_id: Some(span.span_id),
1355            op: Some(op.into()),
1356            description: if description.is_empty() {
1357                None
1358            } else {
1359                Some(description.into())
1360            },
1361            span_id: id,
1362            start_timestamp: timestamp,
1363            ..Default::default()
1364        };
1365        Span {
1366            transaction: self.transaction.clone(),
1367            tracing_state: self.tracing_state,
1368            span: Arc::new(Mutex::new(span)),
1369        }
1370    }
1371}
1372
1373impl PartialEq for Span {
1374    fn eq(&self, other: &Self) -> bool {
1375        Arc::ptr_eq(&self.span, &other.span)
1376    }
1377}
1378
1379/// Represents a key-value pair such as an HTTP header.
1380pub type TraceHeader = (&'static str, String);
1381
1382/// An Iterator over HTTP header names and values needed for distributed tracing.
1383///
1384/// This currently only yields the `sentry-trace` header, but other headers
1385/// may be added in the future.
1386pub struct TraceHeadersIter {
1387    sentry_trace: Option<String>,
1388}
1389
1390impl TraceHeadersIter {
1391    #[cfg(feature = "client")]
1392    pub(crate) fn new(sentry_trace: String) -> Self {
1393        Self {
1394            sentry_trace: Some(sentry_trace),
1395        }
1396    }
1397}
1398
1399impl Iterator for TraceHeadersIter {
1400    type Item = (&'static str, String);
1401
1402    fn next(&mut self) -> Option<Self::Item> {
1403        self.sentry_trace.take().map(|st| ("sentry-trace", st))
1404    }
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409    use std::sync::Arc;
1410
1411    use super::*;
1412
1413    #[test]
1414    fn disabled_forwards_trace_id() {
1415        let headers = [(
1416            "SenTrY-TRAce",
1417            "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1418        )];
1419        let ctx = TransactionContext::continue_from_headers("noop", "noop", headers);
1420        let trx = start_transaction(ctx);
1421
1422        let span = trx.start_child("noop", "noop");
1423
1424        let header = span.iter_headers().next().unwrap().1;
1425        let parsed =
1426            TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();
1427
1428        assert_eq!(
1429            &parsed.trace_id.to_string(),
1430            "09e04486820349518ac7b5d2adbf6ba5"
1431        );
1432        assert_eq!(parsed.sampled, Some(true));
1433    }
1434
1435    #[test]
1436    fn transaction_context_public_getters() {
1437        let mut ctx = TransactionContext::new("test-name", "test-operation");
1438        assert_eq!(ctx.name(), "test-name");
1439        assert_eq!(ctx.operation(), "test-operation");
1440        assert_eq!(ctx.sampled(), None);
1441
1442        ctx.set_sampled(true);
1443        assert_eq!(ctx.sampled(), Some(true));
1444    }
1445
1446    #[test]
1447    fn continue_from_headers_stores_incoming_org_id() {
1448        let ctx = TransactionContext::continue_from_headers(
1449            "noop",
1450            "noop",
1451            [
1452                (
1453                    "sentry-trace",
1454                    "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1455                ),
1456                ("baggage", "sentry-org_id=123"),
1457            ],
1458        );
1459
1460        assert_eq!(
1461            ctx.incoming_trace.map(|incoming| incoming.org_id),
1462            Some(Some("123".parse().unwrap()))
1463        );
1464    }
1465
1466    #[test]
1467    fn continue_from_headers_does_not_keep_org_id_without_sentry_trace() {
1468        let ctx = TransactionContext::continue_from_headers(
1469            "noop",
1470            "noop",
1471            [("baggage", "sentry-org_id=123")],
1472        );
1473
1474        assert!(ctx.incoming_trace.is_none());
1475        assert_eq!(ctx.parent_span_id, None);
1476    }
1477
1478    #[cfg(feature = "client")]
1479    #[test]
1480    fn compute_transaction_sample_rate() {
1481        let ctx = TransactionContext::new("noop", "noop");
1482        assert_eq!(
1483            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1484            Some(0.3)
1485        );
1486        assert_eq!(
1487            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.7), &ctx),
1488            Some(0.7)
1489        );
1490
1491        let mut ctx = TransactionContext::new("noop", "noop");
1492        ctx.set_sampled(true);
1493        assert_eq!(
1494            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1495            Some(1.0)
1496        );
1497        ctx.set_sampled(false);
1498        assert_eq!(
1499            transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1500            Some(0.0)
1501        );
1502
1503        let ctx = TransactionContext::new("noop", "noop");
1504        assert_eq!(
1505            transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1506            None
1507        );
1508        let mut ctx = TransactionContext::new("noop", "noop");
1509        ctx.set_sampled(true);
1510        assert_eq!(
1511            transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1512            None
1513        );
1514        ctx.set_sampled(false);
1515        assert_eq!(
1516            transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1517            None
1518        );
1519
1520        // Function and FixedRate are mutually exclusive strategy variants. A function
1521        // strategy can ignore parent sampling or choose to inspect it.
1522        let mut ctx = TransactionContext::new("noop", "noop");
1523        let sampler = |_: &TransactionContext| 0.7_f32;
1524        let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1525        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1526        ctx.set_sampled(false);
1527        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1528
1529        let sampler = |ctx: &TransactionContext| match ctx.sampled() {
1530            Some(true) => 0.8_f32,
1531            Some(false) => 0.4_f32,
1532            None => 0.6_f32,
1533        };
1534        let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1535        ctx.set_sampled(true);
1536        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.8));
1537        ctx.set_sampled(None);
1538        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.6));
1539
1540        let sampler = |ctx: &TransactionContext| {
1541            if ctx.name() == "must-name" || ctx.operation() == "must-operation" {
1542                return 1.0;
1543            }
1544
1545            if let Some(custom) = ctx.custom() {
1546                if let Some(rate) = custom.get("rate") {
1547                    if let Some(rate) = rate.as_f64() {
1548                        return rate as f32;
1549                    }
1550                }
1551            }
1552
1553            0.1
1554        };
1555        let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1556        let ctx = TransactionContext::new("noop", "must-operation");
1557        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1558        let ctx = TransactionContext::new("must-name", "noop");
1559        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1560        let mut ctx = TransactionContext::new("noop", "noop");
1561        ctx.custom_insert("rate".to_owned(), serde_json::json!(0.7));
1562        assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1563    }
1564}