1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::ops::{Deref, DerefMut};
4use std::sync::{Arc, Mutex, MutexGuard};
5use std::time::SystemTime;
6
7#[cfg(feature = "client")]
8use sentry_types::protocol::v7::client_report::Reason as ClientReportReason;
9#[cfg(feature = "client")]
10use sentry_types::protocol::v7::OrganizationId;
11use sentry_types::protocol::v7::SpanId;
12
13#[cfg(feature = "client")]
14use crate::clientoptions::TracesSamplingStrategy;
15use crate::{protocol, Hub};
16
17#[cfg(feature = "client")]
18use crate::Client;
19
20#[expect(deprecated, reason = "backwards-compatibility re-export")]
21pub use self::headers::{parse_sentry_trace_header as parse_headers, SentryTrace};
22pub use self::headers::{HeaderParseError, TracePropagationContext};
23
24mod headers;
25
26#[cfg(feature = "client")]
27const MAX_SPANS: usize = 1_000;
28
29pub fn start_transaction(ctx: TransactionContext) -> Transaction {
38 #[cfg(feature = "client")]
39 {
40 let client = Hub::with_active(|hub| hub.client());
41 Transaction::new(client, ctx)
42 }
43 #[cfg(not(feature = "client"))]
44 {
45 Transaction::new_noop(ctx)
46 }
47}
48
49pub fn start_transaction_with_timestamp(
56 ctx: TransactionContext,
57 timestamp: SystemTime,
58) -> Transaction {
59 let transaction = start_transaction(ctx);
60 if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
61 tx.start_timestamp = timestamp;
62 }
63 transaction
64}
65
66impl Hub {
69 pub fn start_transaction(&self, ctx: TransactionContext) -> Transaction {
73 #[cfg(feature = "client")]
74 {
75 Transaction::new(self.client(), ctx)
76 }
77 #[cfg(not(feature = "client"))]
78 {
79 Transaction::new_noop(ctx)
80 }
81 }
82
83 pub fn start_transaction_with_timestamp(
87 &self,
88 ctx: TransactionContext,
89 timestamp: SystemTime,
90 ) -> Transaction {
91 let transaction = start_transaction(ctx);
92 if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
93 tx.start_timestamp = timestamp;
94 }
95 transaction
96 }
97}
98
99pub type CustomTransactionContext = serde_json::Map<String, serde_json::Value>;
107
108#[cfg(feature = "client")]
112#[derive(Debug, Clone, Copy)]
113struct IncomingTrace {
114 org_id: Option<OrganizationId>,
115}
116
117#[derive(Debug, Clone)]
122pub struct TransactionContext {
123 #[cfg_attr(not(feature = "client"), allow(dead_code))]
124 name: String,
125 op: String,
126 trace_id: protocol::TraceId,
127 parent_span_id: Option<protocol::SpanId>,
128 span_id: protocol::SpanId,
129 sampled: Option<bool>,
130 #[cfg(feature = "client")]
131 incoming_trace: Option<IncomingTrace>,
132 custom: Option<CustomTransactionContext>,
133}
134
135impl TransactionContext {
136 #[must_use = "this must be used with `start_transaction`"]
148 pub fn new(name: &str, op: &str) -> Self {
149 Self::new_with_trace_id(name, op, protocol::TraceId::default())
150 }
151
152 #[must_use = "this must be used with `start_transaction`"]
159 pub fn new_with_trace_id(name: &str, op: &str, trace_id: protocol::TraceId) -> Self {
160 Self {
161 name: name.into(),
162 op: op.into(),
163 trace_id,
164 parent_span_id: None,
165 span_id: Default::default(),
166 sampled: None,
167 #[cfg(feature = "client")]
168 incoming_trace: None,
169 custom: None,
170 }
171 }
172
173 #[must_use = "this must be used with `start_transaction`"]
181 pub fn new_with_details(
182 name: &str,
183 op: &str,
184 trace_id: protocol::TraceId,
185 span_id: Option<protocol::SpanId>,
186 parent_span_id: Option<protocol::SpanId>,
187 ) -> Self {
188 let mut slf = Self::new_with_trace_id(name, op, trace_id);
189 if let Some(span_id) = span_id {
190 slf.span_id = span_id;
191 }
192 slf.parent_span_id = parent_span_id;
193 slf
194 }
195
196 #[must_use = "this must be used with `start_transaction`"]
201 pub fn continue_from_headers<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(
202 name: &str,
203 op: &str,
204 headers: I,
205 ) -> Self {
206 TracePropagationContext::try_from_headers(headers)
207 .map(|context| Self::continue_from_trace_propagation_context(name, op, &context, None))
208 .unwrap_or_else(|_| Self {
209 name: name.into(),
210 op: op.into(),
211 trace_id: Default::default(),
212 parent_span_id: None,
213 span_id: Default::default(),
214 sampled: None,
215 #[cfg(feature = "client")]
216 incoming_trace: None,
217 custom: None,
218 })
219 }
220
221 #[deprecated = "use `TransactionContext::continue_from_trace_propagation_context` instead"]
224 #[expect(deprecated, reason = "backwards-compatible method")]
225 pub fn continue_from_sentry_trace(
226 name: &str,
227 op: &str,
228 sentry_trace: &SentryTrace,
229 span_id: Option<SpanId>,
230 ) -> Self {
231 let context = (*sentry_trace).into();
232 Self::continue_from_trace_propagation_context(name, op, &context, span_id)
233 }
234
235 pub fn continue_from_trace_propagation_context(
238 name: &str,
239 op: &str,
240 context: &TracePropagationContext,
241 span_id: Option<SpanId>,
242 ) -> Self {
243 let &TracePropagationContext {
244 trace_id,
245 span_id: context_span_id,
246 sampled,
247 #[cfg(feature = "client")]
248 org_id,
249 } = context;
250
251 Self {
252 name: name.into(),
253 op: op.into(),
254 trace_id,
255 parent_span_id: Some(context_span_id),
256 sampled,
257 #[cfg(feature = "client")]
258 incoming_trace: Some(IncomingTrace { org_id }),
259 span_id: span_id.unwrap_or_default(),
260 custom: None,
261 }
262 }
263
264 pub fn continue_from_span(name: &str, op: &str, span: Option<TransactionOrSpan>) -> Self {
270 let span = match span {
271 Some(span) => span,
272 None => return Self::new(name, op),
273 };
274
275 let (trace_id, parent_span_id, sampled) = match span {
276 TransactionOrSpan::Transaction(transaction) => {
277 let inner = transaction.inner.lock().unwrap();
278 (
279 inner.context.trace_id,
280 inner.context.span_id,
281 Some(inner.sampled),
282 )
283 }
284 TransactionOrSpan::Span(span) => {
285 let sampled = span.sampled;
286 let span = span.span.lock().unwrap();
287 (span.trace_id, span.span_id, Some(sampled))
288 }
289 };
290
291 Self {
292 name: name.into(),
293 op: op.into(),
294 trace_id,
295 parent_span_id: Some(parent_span_id),
296 span_id: protocol::SpanId::default(),
297 sampled,
298 #[cfg(feature = "client")]
299 incoming_trace: None,
300 custom: None,
301 }
302 }
303
304 pub fn set_sampled(&mut self, sampled: impl Into<Option<bool>>) {
309 self.sampled = sampled.into();
310 }
311
312 pub fn sampled(&self) -> Option<bool> {
314 self.sampled
315 }
316
317 pub fn name(&self) -> &str {
319 &self.name
320 }
321
322 pub fn operation(&self) -> &str {
324 &self.op
325 }
326
327 pub fn trace_id(&self) -> protocol::TraceId {
329 self.trace_id
330 }
331
332 pub fn span_id(&self) -> protocol::SpanId {
334 self.span_id
335 }
336
337 pub fn custom(&self) -> Option<&CustomTransactionContext> {
339 self.custom.as_ref()
340 }
341
342 pub fn custom_mut(&mut self) -> &mut Option<CustomTransactionContext> {
346 &mut self.custom
347 }
348
349 pub fn custom_insert(
356 &mut self,
357 key: String,
358 value: serde_json::Value,
359 ) -> Option<serde_json::Value> {
360 let mut custom = None;
362 std::mem::swap(&mut self.custom, &mut custom);
363
364 let mut custom = custom.unwrap_or_default();
366
367 let existing_value = custom.insert(key, value);
369 self.custom = Some(custom);
370 existing_value
371 }
372
373 #[must_use]
380 pub fn builder(name: &str, op: &str) -> TransactionContextBuilder {
381 TransactionContextBuilder {
382 ctx: TransactionContext::new(name, op),
383 }
384 }
385
386 #[cfg(feature = "client")]
388 fn reject_incoming_trace(&mut self) {
389 (
390 self.trace_id,
391 self.parent_span_id,
392 self.sampled,
393 self.incoming_trace,
394 ) = Default::default();
395 }
396}
397
398pub struct TransactionContextBuilder {
400 ctx: TransactionContext,
401}
402
403impl TransactionContextBuilder {
404 #[must_use]
406 pub fn with_name(mut self, name: String) -> Self {
407 self.ctx.name = name;
408 self
409 }
410
411 #[must_use]
413 pub fn with_op(mut self, op: String) -> Self {
414 self.ctx.op = op;
415 self
416 }
417
418 #[must_use]
420 pub fn with_trace_id(mut self, trace_id: protocol::TraceId) -> Self {
421 self.ctx.trace_id = trace_id;
422 self
423 }
424
425 #[must_use]
427 pub fn with_parent_span_id(mut self, parent_span_id: Option<protocol::SpanId>) -> Self {
428 self.ctx.parent_span_id = parent_span_id;
429 self
430 }
431
432 #[must_use]
434 pub fn with_span_id(mut self, span_id: protocol::SpanId) -> Self {
435 self.ctx.span_id = span_id;
436 self
437 }
438
439 #[must_use]
441 pub fn with_sampled(mut self, sampled: Option<bool>) -> Self {
442 self.ctx.sampled = sampled;
443 self
444 }
445
446 #[must_use]
448 pub fn with_custom(mut self, key: String, value: serde_json::Value) -> Self {
449 self.ctx.custom_insert(key, value);
450 self
451 }
452
453 pub fn finish(self) -> TransactionContext {
455 self.ctx
456 }
457}
458
459pub type TracesSampler = dyn Fn(&TransactionContext) -> f32 + Send + Sync;
465
466#[derive(Clone, Debug, PartialEq)]
470pub enum TransactionOrSpan {
471 Transaction(Transaction),
473 Span(Span),
475}
476
477impl From<Transaction> for TransactionOrSpan {
478 fn from(transaction: Transaction) -> Self {
479 Self::Transaction(transaction)
480 }
481}
482
483impl From<Span> for TransactionOrSpan {
484 fn from(span: Span) -> Self {
485 Self::Span(span)
486 }
487}
488
489impl TransactionOrSpan {
490 pub fn set_data(&self, key: &str, value: protocol::Value) {
492 match self {
493 TransactionOrSpan::Transaction(transaction) => transaction.set_data(key, value),
494 TransactionOrSpan::Span(span) => span.set_data(key, value),
495 }
496 }
497
498 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
500 match self {
501 TransactionOrSpan::Transaction(transaction) => transaction.set_tag(key, value),
502 TransactionOrSpan::Span(span) => span.set_tag(key, value),
503 }
504 }
505
506 pub fn get_trace_context(&self) -> protocol::TraceContext {
510 match self {
511 TransactionOrSpan::Transaction(transaction) => transaction.get_trace_context(),
512 TransactionOrSpan::Span(span) => span.get_trace_context(),
513 }
514 }
515
516 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
518 match self {
519 TransactionOrSpan::Transaction(transaction) => transaction.get_status(),
520 TransactionOrSpan::Span(span) => span.get_status(),
521 }
522 }
523
524 pub fn set_status(&self, status: protocol::SpanStatus) {
526 match self {
527 TransactionOrSpan::Transaction(transaction) => transaction.set_status(status),
528 TransactionOrSpan::Span(span) => span.set_status(status),
529 }
530 }
531
532 pub fn set_op(&self, op: &str) {
534 match self {
535 TransactionOrSpan::Transaction(transaction) => transaction.set_op(op),
536 TransactionOrSpan::Span(span) => span.set_op(op),
537 }
538 }
539
540 pub fn set_name(&self, name: &str) {
542 match self {
543 TransactionOrSpan::Transaction(transaction) => transaction.set_name(name),
544 TransactionOrSpan::Span(span) => span.set_name(name),
545 }
546 }
547
548 pub fn set_request(&self, request: protocol::Request) {
550 match self {
551 TransactionOrSpan::Transaction(transaction) => transaction.set_request(request),
552 TransactionOrSpan::Span(span) => span.set_request(request),
553 }
554 }
555
556 pub fn iter_headers(&self) -> TraceHeadersIter {
560 match self {
561 TransactionOrSpan::Transaction(transaction) => transaction.iter_headers(),
562 TransactionOrSpan::Span(span) => span.iter_headers(),
563 }
564 }
565
566 pub fn is_sampled(&self) -> bool {
568 match self {
569 TransactionOrSpan::Transaction(transaction) => transaction.is_sampled(),
570 TransactionOrSpan::Span(span) => span.is_sampled(),
571 }
572 }
573
574 #[must_use = "a span must be explicitly closed via `finish()`"]
579 pub fn start_child(&self, op: &str, description: &str) -> Span {
580 match self {
581 TransactionOrSpan::Transaction(transaction) => transaction.start_child(op, description),
582 TransactionOrSpan::Span(span) => span.start_child(op, description),
583 }
584 }
585
586 #[must_use = "a span must be explicitly closed via `finish()`"]
591 pub fn start_child_with_details(
592 &self,
593 op: &str,
594 description: &str,
595 id: SpanId,
596 timestamp: SystemTime,
597 ) -> Span {
598 match self {
599 TransactionOrSpan::Transaction(transaction) => {
600 transaction.start_child_with_details(op, description, id, timestamp)
601 }
602 TransactionOrSpan::Span(span) => {
603 span.start_child_with_details(op, description, id, timestamp)
604 }
605 }
606 }
607
608 #[cfg(feature = "client")]
609 pub(crate) fn apply_to_event(&self, event: &mut protocol::Event<'_>) {
610 if event.contexts.contains_key("trace") {
611 return;
612 }
613
614 let context = match self {
615 TransactionOrSpan::Transaction(transaction) => {
616 transaction.inner.lock().unwrap().context.clone()
617 }
618 TransactionOrSpan::Span(span) => {
619 let span = span.span.lock().unwrap();
620 protocol::TraceContext {
621 span_id: span.span_id,
622 trace_id: span.trace_id,
623 ..Default::default()
624 }
625 }
626 };
627 event.contexts.insert("trace".into(), context.into());
628 }
629
630 pub fn finish_with_timestamp(self, timestamp: SystemTime) {
635 match self {
636 TransactionOrSpan::Transaction(transaction) => {
637 transaction.finish_with_timestamp(timestamp)
638 }
639 TransactionOrSpan::Span(span) => span.finish_with_timestamp(timestamp),
640 }
641 }
642
643 pub fn finish(self) {
648 match self {
649 TransactionOrSpan::Transaction(transaction) => transaction.finish(),
650 TransactionOrSpan::Span(span) => span.finish(),
651 }
652 }
653}
654
655#[derive(Debug)]
656pub(crate) struct TransactionInner {
657 #[cfg(feature = "client")]
658 client: Option<Arc<Client>>,
659 sampled: bool,
660 pub(crate) context: protocol::TraceContext,
661 pub(crate) transaction: Option<protocol::Transaction<'static>>,
662}
663
664type TransactionArc = Arc<Mutex<TransactionInner>>;
665
666#[cfg(feature = "client")]
670fn transaction_sample_rate(
671 traces_sampling_strategy: &TracesSamplingStrategy,
672 ctx: &TransactionContext,
673) -> f32 {
674 match traces_sampling_strategy {
675 &TracesSamplingStrategy::FixedRate(rate) => ctx.sampled.map_or(rate, f32::from),
676 TracesSamplingStrategy::Function(traces_sampler) => traces_sampler(ctx),
677 TracesSamplingStrategy::Disabled => 0.0,
678 }
679}
680
681#[cfg(feature = "client")]
682fn should_continue_trace(
683 incoming: Option<OrganizationId>,
684 sdk: Option<OrganizationId>,
685 strict: bool,
686) -> bool {
687 match (incoming, sdk) {
688 (Some(incoming), Some(sdk)) => incoming == sdk,
689 (Some(_), None) | (None, Some(_)) => !strict,
690 (None, None) => true,
691 }
692}
693
694#[cfg(feature = "client")]
696impl Client {
697 fn determine_sampling_decision(&self, ctx: &TransactionContext) -> (bool, f32) {
698 let client_options = self.options();
699 let sample_rate = transaction_sample_rate(&client_options.traces_sampling_strategy, ctx);
700 let sampled = self.sample_should_send(sample_rate);
701 (sampled, sample_rate)
702 }
703}
704
705#[cfg(feature = "client")]
707#[derive(Clone, Debug)]
708struct TransactionMetadata {
709 sample_rate: f32,
711}
712
713#[derive(Clone, Debug)]
719pub struct Transaction {
720 pub(crate) inner: TransactionArc,
721 #[cfg(feature = "client")]
722 metadata: TransactionMetadata,
723}
724
725pub struct TransactionData<'a>(MutexGuard<'a, TransactionInner>);
727
728impl<'a> TransactionData<'a> {
729 pub fn iter(&self) -> Box<dyn Iterator<Item = (&String, &protocol::Value)> + '_> {
736 if self.0.transaction.is_some() {
737 Box::new(self.0.context.data.iter())
738 } else {
739 Box::new(std::iter::empty())
740 }
741 }
742
743 pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) {
745 if self.0.transaction.is_some() {
746 self.0.context.data.insert(key.into(), value);
747 }
748 }
749
750 pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) {
752 if let Some(transaction) = self.0.transaction.as_mut() {
753 transaction.tags.insert(key.into(), value);
754 }
755 }
756}
757
758impl Transaction {
759 #[cfg(feature = "client")]
760 fn new(client: Option<Arc<Client>>, mut ctx: TransactionContext) -> Self {
761 let ((sampled, sample_rate), transaction) = match client.as_ref() {
762 Some(client) => {
763 let options = client.options();
764 let sdk_org_id = options.org_id.or_else(|| options.dsn.as_ref()?.org_id());
765
766 if ctx.incoming_trace.is_some_and(
767 |IncomingTrace {
768 org_id: incoming_org_id,
769 }| {
770 !should_continue_trace(
771 incoming_org_id,
772 sdk_org_id,
773 options.strict_trace_continuation,
774 )
775 },
776 ) {
777 ctx.reject_incoming_trace();
778 }
779
780 (
781 client.determine_sampling_decision(&ctx),
782 Some(protocol::Transaction {
783 name: Some(ctx.name),
784 ..Default::default()
785 }),
786 )
787 }
788 None => (
789 (
790 ctx.sampled.unwrap_or(false),
791 ctx.sampled.map_or(0.0, f32::from),
792 ),
793 None,
794 ),
795 };
796
797 let context = protocol::TraceContext {
798 trace_id: ctx.trace_id,
799 parent_span_id: ctx.parent_span_id,
800 span_id: ctx.span_id,
801 op: Some(ctx.op),
802 ..Default::default()
803 };
804
805 Self {
806 inner: Arc::new(Mutex::new(TransactionInner {
807 client,
808 sampled,
809 context,
810 transaction,
811 })),
812 metadata: TransactionMetadata { sample_rate },
813 }
814 }
815
816 #[cfg(not(feature = "client"))]
817 fn new_noop(ctx: TransactionContext) -> Self {
818 let context = protocol::TraceContext {
819 trace_id: ctx.trace_id,
820 parent_span_id: ctx.parent_span_id,
821 op: Some(ctx.op),
822 ..Default::default()
823 };
824 let sampled = ctx.sampled.unwrap_or(false);
825
826 Self {
827 inner: Arc::new(Mutex::new(TransactionInner {
828 sampled,
829 context,
830 transaction: None,
831 })),
832 }
833 }
834
835 pub fn set_data(&self, key: &str, value: protocol::Value) {
837 let mut inner = self.inner.lock().unwrap();
838 if inner.transaction.is_some() {
839 inner.context.data.insert(key.into(), value);
840 }
841 }
842
843 pub fn set_extra(&self, key: &str, value: protocol::Value) {
845 let mut inner = self.inner.lock().unwrap();
846 if let Some(transaction) = inner.transaction.as_mut() {
847 transaction.extra.insert(key.into(), value);
848 }
849 }
850
851 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
853 let mut inner = self.inner.lock().unwrap();
854 if let Some(transaction) = inner.transaction.as_mut() {
855 transaction.tags.insert(key.into(), value.to_string());
856 }
857 }
858
859 pub fn data(&self) -> TransactionData<'_> {
869 TransactionData(self.inner.lock().unwrap())
870 }
871
872 pub fn get_trace_context(&self) -> protocol::TraceContext {
876 let inner = self.inner.lock().unwrap();
877 inner.context.clone()
878 }
879
880 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
882 let inner = self.inner.lock().unwrap();
883 inner.context.status
884 }
885
886 pub fn set_status(&self, status: protocol::SpanStatus) {
888 let mut inner = self.inner.lock().unwrap();
889 inner.context.status = Some(status);
890 }
891
892 pub fn set_op(&self, op: &str) {
894 let mut inner = self.inner.lock().unwrap();
895 inner.context.op = Some(op.to_string());
896 }
897
898 pub fn set_name(&self, name: &str) {
900 let mut inner = self.inner.lock().unwrap();
901 if let Some(transaction) = inner.transaction.as_mut() {
902 transaction.name = Some(name.to_string());
903 }
904 }
905
906 pub fn set_request(&self, request: protocol::Request) {
908 let mut inner = self.inner.lock().unwrap();
909 if let Some(transaction) = inner.transaction.as_mut() {
910 transaction.request = Some(request);
911 }
912 }
913
914 pub fn set_origin(&self, origin: &str) {
916 let mut inner = self.inner.lock().unwrap();
917 inner.context.origin = Some(origin.to_owned());
918 }
919
920 pub fn iter_headers(&self) -> TraceHeadersIter {
924 let inner = self.inner.lock().unwrap();
925 let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
926 .with_sampled(inner.sampled);
927 TraceHeadersIter {
928 sentry_trace: Some(trace.sentry_trace_header()),
929 }
930 }
931
932 pub fn is_sampled(&self) -> bool {
934 self.inner.lock().unwrap().sampled
935 }
936
937 pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
942 with_client_impl! {{
943 let mut inner = self.inner.lock().unwrap();
944
945 if !inner.sampled {
947 if let Some(transaction) = inner.transaction.take() {
948 if let Some(client) = inner.client.as_ref() {
949 client.record_lost_data(&transaction, ClientReportReason::SampleRate);
950 }
951 }
952 return;
953 }
954
955 if let Some(mut transaction) = inner.transaction.take() {
956 if let Some(client) = inner.client.take() {
957 transaction.finish_with_timestamp(_timestamp);
958 transaction
959 .contexts
960 .insert("trace".into(), inner.context.clone().into());
961
962 Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction));
963 let opts = client.options();
964 transaction.release.clone_from(&opts.release);
965 transaction.environment.clone_from(&opts.environment);
966 transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone()));
967 transaction.server_name.clone_from(&opts.server_name);
968
969 let mut dsc = protocol::DynamicSamplingContext::new()
970 .with_trace_id(inner.context.trace_id)
971 .with_sample_rate(self.metadata.sample_rate)
972 .with_sampled(inner.sampled);
973 if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) {
974 dsc = dsc.with_public_key(public_key.to_owned());
975 }
976
977 drop(inner);
978
979 let mut envelope = protocol::Envelope::new().with_headers(
980 protocol::EnvelopeHeaders::new().with_trace(dsc)
981 );
982 envelope.add_item(transaction);
983
984 client.send_envelope(envelope)
985 }
986 }
987 }}
988 }
989
990 pub fn finish(self) {
995 self.finish_with_timestamp(SystemTime::now());
996 }
997
998 #[must_use = "a span must be explicitly closed via `finish()`"]
1002 pub fn start_child(&self, op: &str, description: &str) -> Span {
1003 let inner = self.inner.lock().unwrap();
1004 let span = protocol::Span {
1005 trace_id: inner.context.trace_id,
1006 parent_span_id: Some(inner.context.span_id),
1007 op: Some(op.into()),
1008 description: if description.is_empty() {
1009 None
1010 } else {
1011 Some(description.into())
1012 },
1013 ..Default::default()
1014 };
1015 Span {
1016 transaction: Arc::clone(&self.inner),
1017 sampled: inner.sampled,
1018 span: Arc::new(Mutex::new(span)),
1019 }
1020 }
1021
1022 #[must_use = "a span must be explicitly closed via `finish()`"]
1026 pub fn start_child_with_details(
1027 &self,
1028 op: &str,
1029 description: &str,
1030 id: SpanId,
1031 timestamp: SystemTime,
1032 ) -> Span {
1033 let inner = self.inner.lock().unwrap();
1034 let span = protocol::Span {
1035 trace_id: inner.context.trace_id,
1036 parent_span_id: Some(inner.context.span_id),
1037 op: Some(op.into()),
1038 description: if description.is_empty() {
1039 None
1040 } else {
1041 Some(description.into())
1042 },
1043 span_id: id,
1044 start_timestamp: timestamp,
1045 ..Default::default()
1046 };
1047 Span {
1048 transaction: Arc::clone(&self.inner),
1049 sampled: inner.sampled,
1050 span: Arc::new(Mutex::new(span)),
1051 }
1052 }
1053}
1054
1055impl PartialEq for Transaction {
1056 fn eq(&self, other: &Self) -> bool {
1057 Arc::ptr_eq(&self.inner, &other.inner)
1058 }
1059}
1060
1061pub struct Data<'a>(MutexGuard<'a, protocol::Span>);
1063
1064impl Data<'_> {
1065 pub fn set_data(&mut self, key: String, value: protocol::Value) {
1067 self.0.data.insert(key, value);
1068 }
1069
1070 pub fn set_tag(&mut self, key: String, value: String) {
1072 self.0.tags.insert(key, value);
1073 }
1074}
1075
1076impl Deref for Data<'_> {
1077 type Target = BTreeMap<String, protocol::Value>;
1078
1079 fn deref(&self) -> &Self::Target {
1080 &self.0.data
1081 }
1082}
1083
1084impl DerefMut for Data<'_> {
1085 fn deref_mut(&mut self) -> &mut Self::Target {
1086 &mut self.0.data
1087 }
1088}
1089
1090#[derive(Clone, Debug)]
1095pub struct Span {
1096 pub(crate) transaction: TransactionArc,
1097 sampled: bool,
1098 span: SpanArc,
1099}
1100
1101type SpanArc = Arc<Mutex<protocol::Span>>;
1102
1103impl Span {
1104 pub fn set_data(&self, key: &str, value: protocol::Value) {
1106 let mut span = self.span.lock().unwrap();
1107 span.data.insert(key.into(), value);
1108 }
1109
1110 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
1112 let mut span = self.span.lock().unwrap();
1113 span.tags.insert(key.into(), value.to_string());
1114 }
1115
1116 pub fn data(&self) -> Data<'_> {
1128 Data(self.span.lock().unwrap())
1129 }
1130
1131 pub fn get_trace_context(&self) -> protocol::TraceContext {
1135 let transaction = self.transaction.lock().unwrap();
1136 transaction.context.clone()
1137 }
1138
1139 pub fn get_span_id(&self) -> protocol::SpanId {
1141 let span = self.span.lock().unwrap();
1142 span.span_id
1143 }
1144
1145 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
1147 let span = self.span.lock().unwrap();
1148 span.status
1149 }
1150
1151 pub fn set_status(&self, status: protocol::SpanStatus) {
1153 let mut span = self.span.lock().unwrap();
1154 span.status = Some(status);
1155 }
1156
1157 pub fn set_op(&self, op: &str) {
1159 let mut span = self.span.lock().unwrap();
1160 span.op = Some(op.to_string());
1161 }
1162
1163 pub fn set_name(&self, name: &str) {
1165 let mut span = self.span.lock().unwrap();
1166 span.description = Some(name.to_string());
1167 }
1168
1169 pub fn set_request(&self, request: protocol::Request) {
1171 let mut span = self.span.lock().unwrap();
1172 if let Some(method) = request.method {
1174 span.data.insert("method".into(), method.into());
1175 }
1176 if let Some(url) = request.url {
1177 span.data.insert("url".into(), url.to_string().into());
1178 }
1179 if let Some(data) = request.data {
1180 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&data) {
1181 span.data.insert("data".into(), data);
1182 } else {
1183 span.data.insert("data".into(), data.into());
1184 }
1185 }
1186 if let Some(query_string) = request.query_string {
1187 span.data.insert("query_string".into(), query_string.into());
1188 }
1189 if let Some(cookies) = request.cookies {
1190 span.data.insert("cookies".into(), cookies.into());
1191 }
1192 if !request.headers.is_empty() {
1193 if let Ok(headers) = serde_json::to_value(request.headers) {
1194 span.data.insert("headers".into(), headers);
1195 }
1196 }
1197 if !request.env.is_empty() {
1198 if let Ok(env) = serde_json::to_value(request.env) {
1199 span.data.insert("env".into(), env);
1200 }
1201 }
1202 }
1203
1204 pub fn iter_headers(&self) -> TraceHeadersIter {
1208 let span = self.span.lock().unwrap();
1209 let trace =
1210 TracePropagationContext::new(span.trace_id, span.span_id).with_sampled(self.sampled);
1211 TraceHeadersIter {
1212 sentry_trace: Some(trace.sentry_trace_header()),
1213 }
1214 }
1215
1216 pub fn is_sampled(&self) -> bool {
1218 self.sampled
1219 }
1220
1221 pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
1226 with_client_impl! {{
1227 let mut span = self.span.lock().unwrap();
1228 if span.timestamp.is_some() {
1229 return;
1231 }
1232 span.finish_with_timestamp(_timestamp);
1233 let mut inner = self.transaction.lock().unwrap();
1234 if let Some(transaction) = inner.transaction.as_mut() {
1235 if transaction.spans.len() <= MAX_SPANS {
1236 transaction.spans.push(span.clone());
1237 } else if let Some(client) = inner.client.as_ref() {
1238 client.record_lost_data(&*span, ClientReportReason::BufferOverflow);
1239 }
1240 }
1241 }}
1242 }
1243
1244 pub fn finish(self) {
1249 self.finish_with_timestamp(SystemTime::now());
1250 }
1251
1252 #[must_use = "a span must be explicitly closed via `finish()`"]
1256 pub fn start_child(&self, op: &str, description: &str) -> Span {
1257 let span = self.span.lock().unwrap();
1258 let span = protocol::Span {
1259 trace_id: span.trace_id,
1260 parent_span_id: Some(span.span_id),
1261 op: Some(op.into()),
1262 description: if description.is_empty() {
1263 None
1264 } else {
1265 Some(description.into())
1266 },
1267 ..Default::default()
1268 };
1269 Span {
1270 transaction: self.transaction.clone(),
1271 sampled: self.sampled,
1272 span: Arc::new(Mutex::new(span)),
1273 }
1274 }
1275
1276 #[must_use = "a span must be explicitly closed via `finish()`"]
1280 fn start_child_with_details(
1281 &self,
1282 op: &str,
1283 description: &str,
1284 id: SpanId,
1285 timestamp: SystemTime,
1286 ) -> Span {
1287 let span = self.span.lock().unwrap();
1288 let span = protocol::Span {
1289 trace_id: span.trace_id,
1290 parent_span_id: Some(span.span_id),
1291 op: Some(op.into()),
1292 description: if description.is_empty() {
1293 None
1294 } else {
1295 Some(description.into())
1296 },
1297 span_id: id,
1298 start_timestamp: timestamp,
1299 ..Default::default()
1300 };
1301 Span {
1302 transaction: self.transaction.clone(),
1303 sampled: self.sampled,
1304 span: Arc::new(Mutex::new(span)),
1305 }
1306 }
1307}
1308
1309impl PartialEq for Span {
1310 fn eq(&self, other: &Self) -> bool {
1311 Arc::ptr_eq(&self.span, &other.span)
1312 }
1313}
1314
1315pub type TraceHeader = (&'static str, String);
1317
1318pub struct TraceHeadersIter {
1323 sentry_trace: Option<String>,
1324}
1325
1326impl TraceHeadersIter {
1327 #[cfg(feature = "client")]
1328 pub(crate) fn new(sentry_trace: String) -> Self {
1329 Self {
1330 sentry_trace: Some(sentry_trace),
1331 }
1332 }
1333}
1334
1335impl Iterator for TraceHeadersIter {
1336 type Item = (&'static str, String);
1337
1338 fn next(&mut self) -> Option<Self::Item> {
1339 self.sentry_trace.take().map(|st| ("sentry-trace", st))
1340 }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345 use std::sync::Arc;
1346
1347 use super::*;
1348
1349 #[test]
1350 fn disabled_forwards_trace_id() {
1351 let headers = [(
1352 "SenTrY-TRAce",
1353 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1354 )];
1355 let ctx = TransactionContext::continue_from_headers("noop", "noop", headers);
1356 let trx = start_transaction(ctx);
1357
1358 let span = trx.start_child("noop", "noop");
1359
1360 let header = span.iter_headers().next().unwrap().1;
1361 let parsed =
1362 TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();
1363
1364 assert_eq!(
1365 &parsed.trace_id.to_string(),
1366 "09e04486820349518ac7b5d2adbf6ba5"
1367 );
1368 assert_eq!(parsed.sampled, Some(true));
1369 }
1370
1371 #[test]
1372 fn transaction_context_public_getters() {
1373 let mut ctx = TransactionContext::new("test-name", "test-operation");
1374 assert_eq!(ctx.name(), "test-name");
1375 assert_eq!(ctx.operation(), "test-operation");
1376 assert_eq!(ctx.sampled(), None);
1377
1378 ctx.set_sampled(true);
1379 assert_eq!(ctx.sampled(), Some(true));
1380 }
1381
1382 #[test]
1383 fn continue_from_headers_stores_incoming_org_id() {
1384 let ctx = TransactionContext::continue_from_headers(
1385 "noop",
1386 "noop",
1387 [
1388 (
1389 "sentry-trace",
1390 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1391 ),
1392 ("baggage", "sentry-org_id=123"),
1393 ],
1394 );
1395
1396 assert_eq!(
1397 ctx.incoming_trace.map(|incoming| incoming.org_id),
1398 Some(Some("123".parse().unwrap()))
1399 );
1400 }
1401
1402 #[test]
1403 fn continue_from_headers_does_not_keep_org_id_without_sentry_trace() {
1404 let ctx = TransactionContext::continue_from_headers(
1405 "noop",
1406 "noop",
1407 [("baggage", "sentry-org_id=123")],
1408 );
1409
1410 assert!(ctx.incoming_trace.is_none());
1411 assert_eq!(ctx.parent_span_id, None);
1412 }
1413
1414 #[cfg(feature = "client")]
1415 #[test]
1416 fn compute_transaction_sample_rate() {
1417 let ctx = TransactionContext::new("noop", "noop");
1418 assert_eq!(
1419 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1420 0.3
1421 );
1422 assert_eq!(
1423 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.7), &ctx),
1424 0.7
1425 );
1426
1427 let mut ctx = TransactionContext::new("noop", "noop");
1428 ctx.set_sampled(true);
1429 assert_eq!(
1430 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1431 1.0
1432 );
1433 ctx.set_sampled(false);
1434 assert_eq!(
1435 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1436 0.0
1437 );
1438
1439 let ctx = TransactionContext::new("noop", "noop");
1440 assert_eq!(
1441 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1442 0.0
1443 );
1444 let mut ctx = TransactionContext::new("noop", "noop");
1445 ctx.set_sampled(true);
1446 assert_eq!(
1447 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1448 0.0
1449 );
1450 ctx.set_sampled(false);
1451 assert_eq!(
1452 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1453 0.0
1454 );
1455
1456 let mut ctx = TransactionContext::new("noop", "noop");
1459 let sampler = |_: &TransactionContext| 0.7_f32;
1460 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1461 assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.7);
1462 ctx.set_sampled(false);
1463 assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.7);
1464
1465 let sampler = |ctx: &TransactionContext| match ctx.sampled() {
1466 Some(true) => 0.8_f32,
1467 Some(false) => 0.4_f32,
1468 None => 0.6_f32,
1469 };
1470 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1471 ctx.set_sampled(true);
1472 assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.8);
1473 ctx.set_sampled(None);
1474 assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.6);
1475
1476 let sampler = |ctx: &TransactionContext| {
1477 if ctx.name() == "must-name" || ctx.operation() == "must-operation" {
1478 return 1.0;
1479 }
1480
1481 if let Some(custom) = ctx.custom() {
1482 if let Some(rate) = custom.get("rate") {
1483 if let Some(rate) = rate.as_f64() {
1484 return rate as f32;
1485 }
1486 }
1487 }
1488
1489 0.1
1490 };
1491 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1492 let ctx = TransactionContext::new("noop", "must-operation");
1493 assert_eq!(transaction_sample_rate(&strategy, &ctx), 1.0);
1494 let ctx = TransactionContext::new("must-name", "noop");
1495 assert_eq!(transaction_sample_rate(&strategy, &ctx), 1.0);
1496 let mut ctx = TransactionContext::new("noop", "noop");
1497 ctx.custom_insert("rate".to_owned(), serde_json::json!(0.7));
1498 assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.7);
1499 }
1500}