sentry_core/
performance.rs

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