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