Skip to main content

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