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