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