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 self::sampling::FinishAction;
15use self::sampling::TracingState;
16#[cfg(feature = "client")]
17use crate::clientoptions::TracesSamplingStrategy;
18use crate::{protocol, Hub};
19
20#[cfg(feature = "client")]
21use crate::Client;
22
23#[expect(deprecated, reason = "backwards-compatibility re-export")]
24pub use self::headers::{parse_sentry_trace_header as parse_headers, SentryTrace};
25pub use self::headers::{HeaderParseError, TracePropagationContext};
26
27mod headers;
28mod sampling;
29
30#[cfg(feature = "client")]
31const MAX_SPANS: usize = 1_000;
32
33pub fn start_transaction(ctx: TransactionContext) -> Transaction {
42 #[cfg(feature = "client")]
43 {
44 let client = Hub::with_active(|hub| hub.client());
45 Transaction::new(client, ctx)
46 }
47 #[cfg(not(feature = "client"))]
48 {
49 Transaction::new_noop(ctx)
50 }
51}
52
53pub fn start_transaction_with_timestamp(
60 ctx: TransactionContext,
61 timestamp: SystemTime,
62) -> Transaction {
63 let transaction = start_transaction(ctx);
64 if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
65 tx.start_timestamp = timestamp;
66 }
67 transaction
68}
69
70impl Hub {
73 pub fn start_transaction(&self, ctx: TransactionContext) -> Transaction {
77 #[cfg(feature = "client")]
78 {
79 Transaction::new(self.client(), ctx)
80 }
81 #[cfg(not(feature = "client"))]
82 {
83 Transaction::new_noop(ctx)
84 }
85 }
86
87 pub fn start_transaction_with_timestamp(
91 &self,
92 ctx: TransactionContext,
93 timestamp: SystemTime,
94 ) -> Transaction {
95 let transaction = start_transaction(ctx);
96 if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
97 tx.start_timestamp = timestamp;
98 }
99 transaction
100 }
101}
102
103pub type CustomTransactionContext = serde_json::Map<String, serde_json::Value>;
111
112#[cfg(feature = "client")]
116#[derive(Debug, Clone, Copy)]
117struct IncomingTrace {
118 org_id: Option<OrganizationId>,
119}
120
121#[derive(Debug, Clone)]
126pub struct TransactionContext {
127 #[cfg_attr(not(feature = "client"), allow(dead_code))]
128 name: String,
129 op: String,
130 trace_id: protocol::TraceId,
131 parent_span_id: Option<protocol::SpanId>,
132 span_id: protocol::SpanId,
133 sampled: Option<bool>,
134 #[cfg(feature = "client")]
135 incoming_trace: Option<IncomingTrace>,
136 custom: Option<CustomTransactionContext>,
137}
138
139impl TransactionContext {
140 #[must_use = "this must be used with `start_transaction`"]
152 pub fn new(name: &str, op: &str) -> Self {
153 Self::new_with_trace_id(name, op, protocol::TraceId::default())
154 }
155
156 #[must_use = "this must be used with `start_transaction`"]
163 pub fn new_with_trace_id(name: &str, op: &str, trace_id: protocol::TraceId) -> Self {
164 Self {
165 name: name.into(),
166 op: op.into(),
167 trace_id,
168 parent_span_id: None,
169 span_id: Default::default(),
170 sampled: None,
171 #[cfg(feature = "client")]
172 incoming_trace: None,
173 custom: None,
174 }
175 }
176
177 #[must_use = "this must be used with `start_transaction`"]
185 pub fn new_with_details(
186 name: &str,
187 op: &str,
188 trace_id: protocol::TraceId,
189 span_id: Option<protocol::SpanId>,
190 parent_span_id: Option<protocol::SpanId>,
191 ) -> Self {
192 let mut slf = Self::new_with_trace_id(name, op, trace_id);
193 if let Some(span_id) = span_id {
194 slf.span_id = span_id;
195 }
196 slf.parent_span_id = parent_span_id;
197 slf
198 }
199
200 #[must_use = "this must be used with `start_transaction`"]
205 pub fn continue_from_headers<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(
206 name: &str,
207 op: &str,
208 headers: I,
209 ) -> Self {
210 TracePropagationContext::try_from_headers(headers)
211 .map(|context| Self::continue_from_trace_propagation_context(name, op, &context, None))
212 .unwrap_or_else(|_| Self {
213 name: name.into(),
214 op: op.into(),
215 trace_id: Default::default(),
216 parent_span_id: None,
217 span_id: Default::default(),
218 sampled: None,
219 #[cfg(feature = "client")]
220 incoming_trace: None,
221 custom: None,
222 })
223 }
224
225 #[deprecated = "use `TransactionContext::continue_from_trace_propagation_context` instead"]
228 #[expect(deprecated, reason = "backwards-compatible method")]
229 pub fn continue_from_sentry_trace(
230 name: &str,
231 op: &str,
232 sentry_trace: &SentryTrace,
233 span_id: Option<SpanId>,
234 ) -> Self {
235 let context = (*sentry_trace).into();
236 Self::continue_from_trace_propagation_context(name, op, &context, span_id)
237 }
238
239 pub fn continue_from_trace_propagation_context(
242 name: &str,
243 op: &str,
244 context: &TracePropagationContext,
245 span_id: Option<SpanId>,
246 ) -> Self {
247 let &TracePropagationContext {
248 trace_id,
249 span_id: context_span_id,
250 sampled,
251 #[cfg(feature = "client")]
252 org_id,
253 } = context;
254
255 Self {
256 name: name.into(),
257 op: op.into(),
258 trace_id,
259 parent_span_id: Some(context_span_id),
260 sampled,
261 #[cfg(feature = "client")]
262 incoming_trace: Some(IncomingTrace { org_id }),
263 span_id: span_id.unwrap_or_default(),
264 custom: None,
265 }
266 }
267
268 pub fn continue_from_span(name: &str, op: &str, span: Option<TransactionOrSpan>) -> Self {
274 let span = match span {
275 Some(span) => span,
276 None => return Self::new(name, op),
277 };
278
279 let (trace_id, parent_span_id, sampled) = match span {
280 TransactionOrSpan::Transaction(transaction) => {
281 let inner = transaction.inner.lock().unwrap();
282 (
283 inner.context.trace_id,
284 inner.context.span_id,
285 inner.tracing_state.trace_sampled(),
286 )
287 }
288 TransactionOrSpan::Span(span) => {
289 let trace_sampled = span.tracing_state.trace_sampled();
290 let span = span.span.lock().unwrap();
291 (span.trace_id, span.span_id, trace_sampled)
292 }
293 };
294
295 Self {
296 name: name.into(),
297 op: op.into(),
298 trace_id,
299 parent_span_id: Some(parent_span_id),
300 span_id: protocol::SpanId::default(),
301 sampled,
302 #[cfg(feature = "client")]
303 incoming_trace: None,
304 custom: None,
305 }
306 }
307
308 #[expect(clippy::impl_trait_in_params, reason = "existed before lint enabled")]
313 pub fn set_sampled(&mut self, sampled: impl Into<Option<bool>>) {
314 self.sampled = sampled.into();
315 }
316
317 pub fn sampled(&self) -> Option<bool> {
319 self.sampled
320 }
321
322 pub fn name(&self) -> &str {
324 &self.name
325 }
326
327 pub fn operation(&self) -> &str {
329 &self.op
330 }
331
332 pub fn trace_id(&self) -> protocol::TraceId {
334 self.trace_id
335 }
336
337 pub fn span_id(&self) -> protocol::SpanId {
339 self.span_id
340 }
341
342 pub fn custom(&self) -> Option<&CustomTransactionContext> {
344 self.custom.as_ref()
345 }
346
347 pub fn custom_mut(&mut self) -> &mut Option<CustomTransactionContext> {
351 &mut self.custom
352 }
353
354 pub fn custom_insert(
361 &mut self,
362 key: String,
363 value: serde_json::Value,
364 ) -> Option<serde_json::Value> {
365 let mut custom = None;
367 std::mem::swap(&mut self.custom, &mut custom);
368
369 let mut custom = custom.unwrap_or_default();
371
372 let existing_value = custom.insert(key, value);
374 self.custom = Some(custom);
375 existing_value
376 }
377
378 #[must_use]
385 pub fn builder(name: &str, op: &str) -> TransactionContextBuilder {
386 TransactionContextBuilder {
387 ctx: TransactionContext::new(name, op),
388 }
389 }
390
391 #[cfg(feature = "client")]
393 fn reject_incoming_trace(&mut self) {
394 (
395 self.trace_id,
396 self.parent_span_id,
397 self.sampled,
398 self.incoming_trace,
399 ) = Default::default();
400 }
401}
402
403pub struct TransactionContextBuilder {
405 ctx: TransactionContext,
406}
407
408impl TransactionContextBuilder {
409 #[must_use]
411 pub fn with_name(mut self, name: String) -> Self {
412 self.ctx.name = name;
413 self
414 }
415
416 #[must_use]
418 pub fn with_op(mut self, op: String) -> Self {
419 self.ctx.op = op;
420 self
421 }
422
423 #[must_use]
425 pub fn with_trace_id(mut self, trace_id: protocol::TraceId) -> Self {
426 self.ctx.trace_id = trace_id;
427 self
428 }
429
430 #[must_use]
432 pub fn with_parent_span_id(mut self, parent_span_id: Option<protocol::SpanId>) -> Self {
433 self.ctx.parent_span_id = parent_span_id;
434 self
435 }
436
437 #[must_use]
439 pub fn with_span_id(mut self, span_id: protocol::SpanId) -> Self {
440 self.ctx.span_id = span_id;
441 self
442 }
443
444 #[must_use]
446 pub fn with_sampled(mut self, sampled: Option<bool>) -> Self {
447 self.ctx.sampled = sampled;
448 self
449 }
450
451 #[must_use]
453 pub fn with_custom(mut self, key: String, value: serde_json::Value) -> Self {
454 self.ctx.custom_insert(key, value);
455 self
456 }
457
458 pub fn finish(self) -> TransactionContext {
460 self.ctx
461 }
462}
463
464pub type TracesSampler = dyn Fn(&TransactionContext) -> f32 + Send + Sync;
470
471#[derive(Clone, Debug, PartialEq)]
475pub enum TransactionOrSpan {
476 Transaction(Transaction),
478 Span(Span),
480}
481
482impl From<Transaction> for TransactionOrSpan {
483 fn from(transaction: Transaction) -> Self {
484 Self::Transaction(transaction)
485 }
486}
487
488impl From<Span> for TransactionOrSpan {
489 fn from(span: Span) -> Self {
490 Self::Span(span)
491 }
492}
493
494impl TransactionOrSpan {
495 pub fn set_data(&self, key: &str, value: protocol::Value) {
497 match self {
498 TransactionOrSpan::Transaction(transaction) => transaction.set_data(key, value),
499 TransactionOrSpan::Span(span) => span.set_data(key, value),
500 }
501 }
502
503 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
505 match self {
506 TransactionOrSpan::Transaction(transaction) => transaction.set_tag(key, value),
507 TransactionOrSpan::Span(span) => span.set_tag(key, value),
508 }
509 }
510
511 pub fn get_trace_context(&self) -> protocol::TraceContext {
515 match self {
516 TransactionOrSpan::Transaction(transaction) => transaction.get_trace_context(),
517 TransactionOrSpan::Span(span) => span.get_trace_context(),
518 }
519 }
520
521 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
523 match self {
524 TransactionOrSpan::Transaction(transaction) => transaction.get_status(),
525 TransactionOrSpan::Span(span) => span.get_status(),
526 }
527 }
528
529 pub fn set_status(&self, status: protocol::SpanStatus) {
531 match self {
532 TransactionOrSpan::Transaction(transaction) => transaction.set_status(status),
533 TransactionOrSpan::Span(span) => span.set_status(status),
534 }
535 }
536
537 pub fn set_op(&self, op: &str) {
539 match self {
540 TransactionOrSpan::Transaction(transaction) => transaction.set_op(op),
541 TransactionOrSpan::Span(span) => span.set_op(op),
542 }
543 }
544
545 pub fn set_name(&self, name: &str) {
547 match self {
548 TransactionOrSpan::Transaction(transaction) => transaction.set_name(name),
549 TransactionOrSpan::Span(span) => span.set_name(name),
550 }
551 }
552
553 pub fn set_request(&self, request: protocol::Request) {
555 match self {
556 TransactionOrSpan::Transaction(transaction) => transaction.set_request(request),
557 TransactionOrSpan::Span(span) => span.set_request(request),
558 }
559 }
560
561 pub fn iter_headers(&self) -> TraceHeadersIter {
565 match self {
566 TransactionOrSpan::Transaction(transaction) => transaction.iter_headers(),
567 TransactionOrSpan::Span(span) => span.iter_headers(),
568 }
569 }
570
571 #[deprecated = "the returned value may not accurately represent the sampling decision"]
584 pub fn is_sampled(&self) -> bool {
585 match self {
586 TransactionOrSpan::Transaction(transaction) =>
587 {
588 #[expect(deprecated)]
589 transaction.is_sampled()
590 }
591 TransactionOrSpan::Span(span) =>
592 {
593 #[expect(deprecated)]
594 span.is_sampled()
595 }
596 }
597 }
598
599 #[must_use = "a span must be explicitly closed via `finish()`"]
604 pub fn start_child(&self, op: &str, description: &str) -> Span {
605 match self {
606 TransactionOrSpan::Transaction(transaction) => transaction.start_child(op, description),
607 TransactionOrSpan::Span(span) => span.start_child(op, description),
608 }
609 }
610
611 #[must_use = "a span must be explicitly closed via `finish()`"]
616 pub fn start_child_with_details(
617 &self,
618 op: &str,
619 description: &str,
620 id: SpanId,
621 timestamp: SystemTime,
622 ) -> Span {
623 match self {
624 TransactionOrSpan::Transaction(transaction) => {
625 transaction.start_child_with_details(op, description, id, timestamp)
626 }
627 TransactionOrSpan::Span(span) => {
628 span.start_child_with_details(op, description, id, timestamp)
629 }
630 }
631 }
632
633 #[cfg(feature = "client")]
634 pub(crate) fn apply_to_event(&self, event: &mut protocol::Event<'_>) {
635 if event.contexts.contains_key("trace") {
636 return;
637 }
638
639 let context = match self {
640 TransactionOrSpan::Transaction(transaction) => {
641 transaction.inner.lock().unwrap().context.clone()
642 }
643 TransactionOrSpan::Span(span) => {
644 let span = span.span.lock().unwrap();
645 protocol::TraceContext {
646 span_id: span.span_id,
647 trace_id: span.trace_id,
648 ..Default::default()
649 }
650 }
651 };
652 event.contexts.insert("trace".into(), context.into());
653 }
654
655 pub fn finish_with_timestamp(self, timestamp: SystemTime) {
660 match self {
661 TransactionOrSpan::Transaction(transaction) => {
662 transaction.finish_with_timestamp(timestamp)
663 }
664 TransactionOrSpan::Span(span) => span.finish_with_timestamp(timestamp),
665 }
666 }
667
668 pub fn finish(self) {
673 match self {
674 TransactionOrSpan::Transaction(transaction) => transaction.finish(),
675 TransactionOrSpan::Span(span) => span.finish(),
676 }
677 }
678}
679
680#[derive(Debug)]
681pub(crate) struct TransactionInner {
682 #[cfg(feature = "client")]
683 client: Option<Arc<Client>>,
684 tracing_state: TracingState,
685 pub(crate) context: protocol::TraceContext,
686 pub(crate) transaction: Option<protocol::Transaction<'static>>,
687}
688
689type TransactionArc = Arc<Mutex<TransactionInner>>;
690
691#[cfg(feature = "client")]
695fn transaction_sample_rate(
696 traces_sampling_strategy: &TracesSamplingStrategy,
697 ctx: &TransactionContext,
698) -> Option<f32> {
699 match traces_sampling_strategy {
700 &TracesSamplingStrategy::FixedRate(rate) => Some(ctx.sampled.map_or(rate, f32::from)),
701 TracesSamplingStrategy::Function(traces_sampler) => Some(traces_sampler(ctx)),
702 TracesSamplingStrategy::Disabled => None,
703 }
704}
705
706#[cfg(feature = "client")]
707fn should_continue_trace(
708 incoming: Option<OrganizationId>,
709 sdk: Option<OrganizationId>,
710 strict: bool,
711) -> bool {
712 match (incoming, sdk) {
713 (Some(incoming), Some(sdk)) => incoming == sdk,
714 (Some(_), None) | (None, Some(_)) => !strict,
715 (None, None) => true,
716 }
717}
718
719#[cfg(feature = "client")]
721impl Client {
722 fn determine_tracing_state(&self, ctx: &TransactionContext) -> TracingState {
726 let client_options = self.options();
727 match transaction_sample_rate(&client_options.traces_sampling_strategy, ctx) {
728 Some(sample_rate) => {
730 let sampled = self.sample_should_send(sample_rate);
731 TracingState::new_enabled(sampled, sample_rate)
732 }
733 None => TracingState::new_disabled(ctx.sampled),
735 }
736 }
737}
738
739#[derive(Clone, Debug)]
745pub struct Transaction {
746 pub(crate) inner: TransactionArc,
747}
748
749pub struct TransactionData<'a>(MutexGuard<'a, TransactionInner>);
751
752impl<'a> TransactionData<'a> {
753 pub fn iter(&self) -> Box<dyn Iterator<Item = (&String, &protocol::Value)> + '_> {
759 if self.0.transaction.is_some() {
760 Box::new(self.0.context.data.iter())
761 } else {
762 Box::new(std::iter::empty())
763 }
764 }
765
766 pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) {
768 if self.0.transaction.is_some() {
769 self.0.context.data.insert(key.into(), value);
770 }
771 }
772
773 pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) {
775 if let Some(transaction) = self.0.transaction.as_mut() {
776 transaction.tags.insert(key.into(), value);
777 }
778 }
779}
780
781impl Transaction {
782 #[cfg(feature = "client")]
783 fn new(client: Option<Arc<Client>>, mut ctx: TransactionContext) -> Self {
784 let (tracing_state, transaction) = match client.as_ref() {
785 Some(client) => {
786 let options = client.options();
787 let sdk_org_id = options.org_id.or_else(|| options.dsn.as_ref()?.org_id());
788
789 if ctx.incoming_trace.is_some_and(
790 |IncomingTrace {
791 org_id: incoming_org_id,
792 }| {
793 !should_continue_trace(
794 incoming_org_id,
795 sdk_org_id,
796 options.strict_trace_continuation,
797 )
798 },
799 ) {
800 ctx.reject_incoming_trace();
801 }
802
803 (
804 client.determine_tracing_state(&ctx),
805 Some(protocol::Transaction {
806 name: Some(ctx.name),
807 ..Default::default()
808 }),
809 )
810 }
811 None => (TracingState::new_disabled(ctx.sampled), None),
812 };
813
814 let context = protocol::TraceContext {
815 trace_id: ctx.trace_id,
816 parent_span_id: ctx.parent_span_id,
817 span_id: ctx.span_id,
818 op: Some(ctx.op),
819 ..Default::default()
820 };
821
822 Self {
823 inner: Arc::new(Mutex::new(TransactionInner {
824 client,
825 tracing_state,
826 context,
827 transaction,
828 })),
829 }
830 }
831
832 #[cfg(not(feature = "client"))]
833 fn new_noop(ctx: TransactionContext) -> Self {
834 let context = protocol::TraceContext {
835 trace_id: ctx.trace_id,
836 parent_span_id: ctx.parent_span_id,
837 op: Some(ctx.op),
838 ..Default::default()
839 };
840 let tracing_state = TracingState::new_disabled(ctx.sampled);
841
842 Self {
843 inner: Arc::new(Mutex::new(TransactionInner {
844 tracing_state,
845 context,
846 transaction: None,
847 })),
848 }
849 }
850
851 pub fn set_data(&self, key: &str, value: protocol::Value) {
853 let mut inner = self.inner.lock().unwrap();
854 if inner.transaction.is_some() {
855 inner.context.data.insert(key.into(), value);
856 }
857 }
858
859 pub fn set_extra(&self, key: &str, value: protocol::Value) {
861 let mut inner = self.inner.lock().unwrap();
862 if let Some(transaction) = inner.transaction.as_mut() {
863 transaction.extra.insert(key.into(), value);
864 }
865 }
866
867 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
869 let mut inner = self.inner.lock().unwrap();
870 if let Some(transaction) = inner.transaction.as_mut() {
871 transaction.tags.insert(key.into(), value.to_string());
872 }
873 }
874
875 pub fn data(&self) -> TransactionData<'_> {
885 TransactionData(self.inner.lock().unwrap())
886 }
887
888 pub fn get_trace_context(&self) -> protocol::TraceContext {
892 let inner = self.inner.lock().unwrap();
893 inner.context.clone()
894 }
895
896 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
898 let inner = self.inner.lock().unwrap();
899 inner.context.status
900 }
901
902 pub fn set_status(&self, status: protocol::SpanStatus) {
904 let mut inner = self.inner.lock().unwrap();
905 inner.context.status = Some(status);
906 }
907
908 pub fn set_op(&self, op: &str) {
910 let mut inner = self.inner.lock().unwrap();
911 inner.context.op = Some(op.to_string());
912 }
913
914 pub fn set_name(&self, name: &str) {
916 let mut inner = self.inner.lock().unwrap();
917 if let Some(transaction) = inner.transaction.as_mut() {
918 transaction.name = Some(name.to_string());
919 }
920 }
921
922 pub fn set_request(&self, request: protocol::Request) {
924 let mut inner = self.inner.lock().unwrap();
925 if let Some(transaction) = inner.transaction.as_mut() {
926 transaction.request = Some(request);
927 }
928 }
929
930 pub fn set_origin(&self, origin: &str) {
932 let mut inner = self.inner.lock().unwrap();
933 inner.context.origin = Some(origin.to_owned());
934 }
935
936 pub fn iter_headers(&self) -> TraceHeadersIter {
940 let inner = self.inner.lock().unwrap();
941 let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
942 .with_maybe_sampled(inner.tracing_state.trace_sampled());
943 TraceHeadersIter {
944 sentry_trace: Some(trace.sentry_trace_header()),
945 }
946 }
947
948 #[deprecated = "the returned value may not accurately represent the sampling decision"]
961 pub fn is_sampled(&self) -> bool {
962 #[cfg(feature = "client")]
966 {
967 matches!(
968 self.inner.lock().unwrap().tracing_state.finish_action(),
969 FinishAction::Send { .. }
970 )
971 }
972
973 #[cfg(not(feature = "client"))]
974 false
975 }
976
977 pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
982 with_client_impl! {{
983 let mut inner = self.inner.lock().unwrap();
984
985 if let (Some(mut transaction), Some(client)) = (inner.transaction.take(), inner.client.take()) {
986 match inner.tracing_state.finish_action() {
987 FinishAction::Send { sample_rate } => {
988 transaction.finish_with_timestamp(_timestamp);
989 transaction
990 .contexts
991 .insert("trace".into(), inner.context.clone().into());
992
993 Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction));
994 let opts = client.options();
995 transaction.release.clone_from(&opts.release);
996 transaction.environment.clone_from(&opts.environment);
997 transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone()));
998 transaction.server_name.clone_from(&opts.server_name);
999
1000 let mut dsc = protocol::DynamicSamplingContext::new()
1001 .with_trace_id(inner.context.trace_id)
1002 .with_sample_rate(sample_rate)
1003 .with_sampled(true);
1004 if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) {
1005 dsc = dsc.with_public_key(public_key.to_owned());
1006 }
1007
1008 drop(inner);
1009
1010 let mut envelope = protocol::Envelope::new().with_headers(
1011 protocol::EnvelopeHeaders::new().with_trace(dsc)
1012 );
1013 envelope.add_item(transaction);
1014
1015 client.send_envelope(envelope);
1016 },
1017 FinishAction::Discard => {
1018 client.record_lost_data(&transaction, ClientReportReason::SampleRate);
1019 },
1020 FinishAction::Ignore => (),
1021 }
1022 }
1023 }}
1024 }
1025
1026 pub fn finish(self) {
1031 self.finish_with_timestamp(SystemTime::now());
1032 }
1033
1034 #[must_use = "a span must be explicitly closed via `finish()`"]
1038 pub fn start_child(&self, op: &str, description: &str) -> Span {
1039 let inner = self.inner.lock().unwrap();
1040 let span = protocol::Span {
1041 trace_id: inner.context.trace_id,
1042 parent_span_id: Some(inner.context.span_id),
1043 op: Some(op.into()),
1044 description: if description.is_empty() {
1045 None
1046 } else {
1047 Some(description.into())
1048 },
1049 ..Default::default()
1050 };
1051 Span {
1052 transaction: Arc::clone(&self.inner),
1053 tracing_state: inner.tracing_state,
1054 span: Arc::new(Mutex::new(span)),
1055 }
1056 }
1057
1058 #[must_use = "a span must be explicitly closed via `finish()`"]
1062 pub fn start_child_with_details(
1063 &self,
1064 op: &str,
1065 description: &str,
1066 id: SpanId,
1067 timestamp: SystemTime,
1068 ) -> Span {
1069 let inner = self.inner.lock().unwrap();
1070 let span = protocol::Span {
1071 trace_id: inner.context.trace_id,
1072 parent_span_id: Some(inner.context.span_id),
1073 op: Some(op.into()),
1074 description: if description.is_empty() {
1075 None
1076 } else {
1077 Some(description.into())
1078 },
1079 span_id: id,
1080 start_timestamp: timestamp,
1081 ..Default::default()
1082 };
1083 Span {
1084 transaction: Arc::clone(&self.inner),
1085 tracing_state: inner.tracing_state,
1086 span: Arc::new(Mutex::new(span)),
1087 }
1088 }
1089}
1090
1091impl PartialEq for Transaction {
1092 fn eq(&self, other: &Self) -> bool {
1093 Arc::ptr_eq(&self.inner, &other.inner)
1094 }
1095}
1096
1097pub struct Data<'a>(MutexGuard<'a, protocol::Span>);
1099
1100impl Data<'_> {
1101 pub fn set_data(&mut self, key: String, value: protocol::Value) {
1103 self.0.data.insert(key, value);
1104 }
1105
1106 pub fn set_tag(&mut self, key: String, value: String) {
1108 self.0.tags.insert(key, value);
1109 }
1110}
1111
1112impl Deref for Data<'_> {
1113 type Target = BTreeMap<String, protocol::Value>;
1114
1115 fn deref(&self) -> &Self::Target {
1116 &self.0.data
1117 }
1118}
1119
1120impl DerefMut for Data<'_> {
1121 fn deref_mut(&mut self) -> &mut Self::Target {
1122 &mut self.0.data
1123 }
1124}
1125
1126#[derive(Clone, Debug)]
1131pub struct Span {
1132 pub(crate) transaction: TransactionArc,
1133 tracing_state: TracingState,
1134 span: SpanArc,
1135}
1136
1137type SpanArc = Arc<Mutex<protocol::Span>>;
1138
1139impl Span {
1140 pub fn set_data(&self, key: &str, value: protocol::Value) {
1142 let mut span = self.span.lock().unwrap();
1143 span.data.insert(key.into(), value);
1144 }
1145
1146 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
1148 let mut span = self.span.lock().unwrap();
1149 span.tags.insert(key.into(), value.to_string());
1150 }
1151
1152 pub fn data(&self) -> Data<'_> {
1164 Data(self.span.lock().unwrap())
1165 }
1166
1167 pub fn get_trace_context(&self) -> protocol::TraceContext {
1171 let transaction = self.transaction.lock().unwrap();
1172 transaction.context.clone()
1173 }
1174
1175 pub fn get_span_id(&self) -> protocol::SpanId {
1177 let span = self.span.lock().unwrap();
1178 span.span_id
1179 }
1180
1181 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
1183 let span = self.span.lock().unwrap();
1184 span.status
1185 }
1186
1187 pub fn set_status(&self, status: protocol::SpanStatus) {
1189 let mut span = self.span.lock().unwrap();
1190 span.status = Some(status);
1191 }
1192
1193 pub fn set_op(&self, op: &str) {
1195 let mut span = self.span.lock().unwrap();
1196 span.op = Some(op.to_string());
1197 }
1198
1199 pub fn set_name(&self, name: &str) {
1201 let mut span = self.span.lock().unwrap();
1202 span.description = Some(name.to_string());
1203 }
1204
1205 pub fn set_request(&self, request: protocol::Request) {
1207 let mut span = self.span.lock().unwrap();
1208 if let Some(method) = request.method {
1210 span.data.insert("method".into(), method.into());
1211 }
1212 if let Some(url) = request.url {
1213 span.data.insert("url".into(), url.to_string().into());
1214 }
1215 if let Some(data) = request.data {
1216 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&data) {
1217 span.data.insert("data".into(), data);
1218 } else {
1219 span.data.insert("data".into(), data.into());
1220 }
1221 }
1222 if let Some(query_string) = request.query_string {
1223 span.data.insert("query_string".into(), query_string.into());
1224 }
1225 if let Some(cookies) = request.cookies {
1226 span.data.insert("cookies".into(), cookies.into());
1227 }
1228 if !request.headers.is_empty() {
1229 if let Ok(headers) = serde_json::to_value(request.headers) {
1230 span.data.insert("headers".into(), headers);
1231 }
1232 }
1233 if !request.env.is_empty() {
1234 if let Ok(env) = serde_json::to_value(request.env) {
1235 span.data.insert("env".into(), env);
1236 }
1237 }
1238 }
1239
1240 pub fn iter_headers(&self) -> TraceHeadersIter {
1244 let span = self.span.lock().unwrap();
1245 let trace = TracePropagationContext::new(span.trace_id, span.span_id)
1246 .with_maybe_sampled(self.tracing_state.trace_sampled());
1247
1248 TraceHeadersIter {
1249 sentry_trace: Some(trace.sentry_trace_header()),
1250 }
1251 }
1252
1253 #[deprecated = "the returned value may not accurately represent the sampling decision"]
1265 pub fn is_sampled(&self) -> bool {
1266 #[cfg(feature = "client")]
1270 {
1271 matches!(
1272 self.tracing_state.finish_action(),
1273 FinishAction::Send { .. }
1274 )
1275 }
1276
1277 #[cfg(not(feature = "client"))]
1278 false
1279 }
1280
1281 pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
1286 with_client_impl! {{
1287 let mut span = self.span.lock().unwrap();
1288 if span.timestamp.is_some() {
1289 return;
1291 }
1292 span.finish_with_timestamp(_timestamp);
1293 let mut inner = self.transaction.lock().unwrap();
1294 if matches!(inner.tracing_state.finish_action(), FinishAction::Ignore) {
1296 return;
1297 }
1298 if let Some(transaction) = inner.transaction.as_mut() {
1299 if transaction.spans.len() <= MAX_SPANS {
1300 transaction.spans.push(span.clone());
1301 } else if let Some(client) = inner.client.as_ref() {
1302 client.record_lost_data(&*span, ClientReportReason::BufferOverflow);
1303 }
1304 }
1305 }}
1306 }
1307
1308 pub fn finish(self) {
1313 self.finish_with_timestamp(SystemTime::now());
1314 }
1315
1316 #[must_use = "a span must be explicitly closed via `finish()`"]
1320 pub fn start_child(&self, op: &str, description: &str) -> Span {
1321 let span = self.span.lock().unwrap();
1322 let span = protocol::Span {
1323 trace_id: span.trace_id,
1324 parent_span_id: Some(span.span_id),
1325 op: Some(op.into()),
1326 description: if description.is_empty() {
1327 None
1328 } else {
1329 Some(description.into())
1330 },
1331 ..Default::default()
1332 };
1333 Span {
1334 transaction: self.transaction.clone(),
1335 tracing_state: self.tracing_state,
1336 span: Arc::new(Mutex::new(span)),
1337 }
1338 }
1339
1340 #[must_use = "a span must be explicitly closed via `finish()`"]
1344 fn start_child_with_details(
1345 &self,
1346 op: &str,
1347 description: &str,
1348 id: SpanId,
1349 timestamp: SystemTime,
1350 ) -> Span {
1351 let span = self.span.lock().unwrap();
1352 let span = protocol::Span {
1353 trace_id: span.trace_id,
1354 parent_span_id: Some(span.span_id),
1355 op: Some(op.into()),
1356 description: if description.is_empty() {
1357 None
1358 } else {
1359 Some(description.into())
1360 },
1361 span_id: id,
1362 start_timestamp: timestamp,
1363 ..Default::default()
1364 };
1365 Span {
1366 transaction: self.transaction.clone(),
1367 tracing_state: self.tracing_state,
1368 span: Arc::new(Mutex::new(span)),
1369 }
1370 }
1371}
1372
1373impl PartialEq for Span {
1374 fn eq(&self, other: &Self) -> bool {
1375 Arc::ptr_eq(&self.span, &other.span)
1376 }
1377}
1378
1379pub type TraceHeader = (&'static str, String);
1381
1382pub struct TraceHeadersIter {
1387 sentry_trace: Option<String>,
1388}
1389
1390impl TraceHeadersIter {
1391 #[cfg(feature = "client")]
1392 pub(crate) fn new(sentry_trace: String) -> Self {
1393 Self {
1394 sentry_trace: Some(sentry_trace),
1395 }
1396 }
1397}
1398
1399impl Iterator for TraceHeadersIter {
1400 type Item = (&'static str, String);
1401
1402 fn next(&mut self) -> Option<Self::Item> {
1403 self.sentry_trace.take().map(|st| ("sentry-trace", st))
1404 }
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409 use std::sync::Arc;
1410
1411 use super::*;
1412
1413 #[test]
1414 fn disabled_forwards_trace_id() {
1415 let headers = [(
1416 "SenTrY-TRAce",
1417 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1418 )];
1419 let ctx = TransactionContext::continue_from_headers("noop", "noop", headers);
1420 let trx = start_transaction(ctx);
1421
1422 let span = trx.start_child("noop", "noop");
1423
1424 let header = span.iter_headers().next().unwrap().1;
1425 let parsed =
1426 TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();
1427
1428 assert_eq!(
1429 &parsed.trace_id.to_string(),
1430 "09e04486820349518ac7b5d2adbf6ba5"
1431 );
1432 assert_eq!(parsed.sampled, Some(true));
1433 }
1434
1435 #[test]
1436 fn transaction_context_public_getters() {
1437 let mut ctx = TransactionContext::new("test-name", "test-operation");
1438 assert_eq!(ctx.name(), "test-name");
1439 assert_eq!(ctx.operation(), "test-operation");
1440 assert_eq!(ctx.sampled(), None);
1441
1442 ctx.set_sampled(true);
1443 assert_eq!(ctx.sampled(), Some(true));
1444 }
1445
1446 #[test]
1447 fn continue_from_headers_stores_incoming_org_id() {
1448 let ctx = TransactionContext::continue_from_headers(
1449 "noop",
1450 "noop",
1451 [
1452 (
1453 "sentry-trace",
1454 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1455 ),
1456 ("baggage", "sentry-org_id=123"),
1457 ],
1458 );
1459
1460 assert_eq!(
1461 ctx.incoming_trace.map(|incoming| incoming.org_id),
1462 Some(Some("123".parse().unwrap()))
1463 );
1464 }
1465
1466 #[test]
1467 fn continue_from_headers_does_not_keep_org_id_without_sentry_trace() {
1468 let ctx = TransactionContext::continue_from_headers(
1469 "noop",
1470 "noop",
1471 [("baggage", "sentry-org_id=123")],
1472 );
1473
1474 assert!(ctx.incoming_trace.is_none());
1475 assert_eq!(ctx.parent_span_id, None);
1476 }
1477
1478 #[cfg(feature = "client")]
1479 #[test]
1480 fn compute_transaction_sample_rate() {
1481 let ctx = TransactionContext::new("noop", "noop");
1482 assert_eq!(
1483 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1484 Some(0.3)
1485 );
1486 assert_eq!(
1487 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.7), &ctx),
1488 Some(0.7)
1489 );
1490
1491 let mut ctx = TransactionContext::new("noop", "noop");
1492 ctx.set_sampled(true);
1493 assert_eq!(
1494 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1495 Some(1.0)
1496 );
1497 ctx.set_sampled(false);
1498 assert_eq!(
1499 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1500 Some(0.0)
1501 );
1502
1503 let ctx = TransactionContext::new("noop", "noop");
1504 assert_eq!(
1505 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1506 None
1507 );
1508 let mut ctx = TransactionContext::new("noop", "noop");
1509 ctx.set_sampled(true);
1510 assert_eq!(
1511 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1512 None
1513 );
1514 ctx.set_sampled(false);
1515 assert_eq!(
1516 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1517 None
1518 );
1519
1520 let mut ctx = TransactionContext::new("noop", "noop");
1523 let sampler = |_: &TransactionContext| 0.7_f32;
1524 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1525 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1526 ctx.set_sampled(false);
1527 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1528
1529 let sampler = |ctx: &TransactionContext| match ctx.sampled() {
1530 Some(true) => 0.8_f32,
1531 Some(false) => 0.4_f32,
1532 None => 0.6_f32,
1533 };
1534 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1535 ctx.set_sampled(true);
1536 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.8));
1537 ctx.set_sampled(None);
1538 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.6));
1539
1540 let sampler = |ctx: &TransactionContext| {
1541 if ctx.name() == "must-name" || ctx.operation() == "must-operation" {
1542 return 1.0;
1543 }
1544
1545 if let Some(custom) = ctx.custom() {
1546 if let Some(rate) = custom.get("rate") {
1547 if let Some(rate) = rate.as_f64() {
1548 return rate as f32;
1549 }
1550 }
1551 }
1552
1553 0.1
1554 };
1555 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1556 let ctx = TransactionContext::new("noop", "must-operation");
1557 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1558 let ctx = TransactionContext::new("must-name", "noop");
1559 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1560 let mut ctx = TransactionContext::new("noop", "noop");
1561 ctx.custom_insert("rate".to_owned(), serde_json::json!(0.7));
1562 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1563 }
1564}