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)> + '_> {
760 if self.0.transaction.is_some() {
761 Box::new(self.0.context.data.iter())
762 } else {
763 Box::new(std::iter::empty())
764 }
765 }
766
767 pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) {
769 if self.0.transaction.is_some() {
770 self.0.context.data.insert(key.into(), value);
771 }
772 }
773
774 pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) {
776 if let Some(transaction) = self.0.transaction.as_mut() {
777 transaction.tags.insert(key.into(), value);
778 }
779 }
780}
781
782impl Transaction {
783 #[cfg(feature = "client")]
784 fn new(client: Option<Arc<Client>>, mut ctx: TransactionContext) -> Self {
785 let (tracing_state, transaction) = match client.as_ref() {
786 Some(client) => {
787 let options = client.options();
788 let sdk_org_id = options.org_id.or_else(|| options.dsn.as_ref()?.org_id());
789
790 if ctx.incoming_trace.is_some_and(
791 |IncomingTrace {
792 org_id: incoming_org_id,
793 }| {
794 !should_continue_trace(
795 incoming_org_id,
796 sdk_org_id,
797 options.strict_trace_continuation,
798 )
799 },
800 ) {
801 ctx.reject_incoming_trace();
802 }
803
804 (
805 client.determine_tracing_state(&ctx),
806 Some(protocol::Transaction {
807 name: Some(ctx.name),
808 ..Default::default()
809 }),
810 )
811 }
812 None => (TracingState::new_disabled(ctx.sampled), None),
813 };
814
815 let context = protocol::TraceContext {
816 trace_id: ctx.trace_id,
817 parent_span_id: ctx.parent_span_id,
818 span_id: ctx.span_id,
819 op: Some(ctx.op),
820 ..Default::default()
821 };
822
823 Self {
824 inner: Arc::new(Mutex::new(TransactionInner {
825 client,
826 tracing_state,
827 context,
828 transaction,
829 })),
830 }
831 }
832
833 #[cfg(not(feature = "client"))]
834 fn new_noop(ctx: TransactionContext) -> Self {
835 let context = protocol::TraceContext {
836 trace_id: ctx.trace_id,
837 parent_span_id: ctx.parent_span_id,
838 op: Some(ctx.op),
839 ..Default::default()
840 };
841 let tracing_state = TracingState::new_disabled(ctx.sampled);
842
843 Self {
844 inner: Arc::new(Mutex::new(TransactionInner {
845 tracing_state,
846 context,
847 transaction: None,
848 })),
849 }
850 }
851
852 pub fn set_data(&self, key: &str, value: protocol::Value) {
854 let mut inner = self.inner.lock().unwrap();
855 if inner.transaction.is_some() {
856 inner.context.data.insert(key.into(), value);
857 }
858 }
859
860 pub fn set_extra(&self, key: &str, value: protocol::Value) {
862 let mut inner = self.inner.lock().unwrap();
863 if let Some(transaction) = inner.transaction.as_mut() {
864 transaction.extra.insert(key.into(), value);
865 }
866 }
867
868 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
870 let mut inner = self.inner.lock().unwrap();
871 if let Some(transaction) = inner.transaction.as_mut() {
872 transaction.tags.insert(key.into(), value.to_string());
873 }
874 }
875
876 pub fn data(&self) -> TransactionData<'_> {
886 TransactionData(self.inner.lock().unwrap())
887 }
888
889 pub fn get_trace_context(&self) -> protocol::TraceContext {
893 let inner = self.inner.lock().unwrap();
894 inner.context.clone()
895 }
896
897 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
899 let inner = self.inner.lock().unwrap();
900 inner.context.status
901 }
902
903 pub fn set_status(&self, status: protocol::SpanStatus) {
905 let mut inner = self.inner.lock().unwrap();
906 inner.context.status = Some(status);
907 }
908
909 pub fn set_op(&self, op: &str) {
911 let mut inner = self.inner.lock().unwrap();
912 inner.context.op = Some(op.to_string());
913 }
914
915 pub fn set_name(&self, name: &str) {
917 let mut inner = self.inner.lock().unwrap();
918 if let Some(transaction) = inner.transaction.as_mut() {
919 transaction.name = Some(name.to_string());
920 }
921 }
922
923 pub fn set_request(&self, request: protocol::Request) {
925 let mut inner = self.inner.lock().unwrap();
926 if let Some(transaction) = inner.transaction.as_mut() {
927 transaction.request = Some(request);
928 }
929 }
930
931 pub fn set_origin(&self, origin: &str) {
933 let mut inner = self.inner.lock().unwrap();
934 inner.context.origin = Some(origin.to_owned());
935 }
936
937 pub fn iter_headers(&self) -> TraceHeadersIter {
941 let inner = self.inner.lock().unwrap();
942 let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
943 .with_maybe_sampled(inner.tracing_state.trace_sampled());
944 TraceHeadersIter {
945 sentry_trace: Some(trace.sentry_trace_header()),
946 }
947 }
948
949 #[deprecated = "the returned value may not accurately represent the sampling decision"]
962 pub fn is_sampled(&self) -> bool {
963 #[cfg(feature = "client")]
967 {
968 matches!(
969 self.inner.lock().unwrap().tracing_state.finish_action(),
970 FinishAction::Send { .. }
971 )
972 }
973
974 #[cfg(not(feature = "client"))]
975 false
976 }
977
978 pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
983 with_client_impl! {{
984 let mut inner = self.inner.lock().unwrap();
985
986 if let (Some(mut transaction), Some(client)) = (inner.transaction.take(), inner.client.take()) {
987 match inner.tracing_state.finish_action() {
988 FinishAction::Send { sample_rate } => {
989 transaction.finish_with_timestamp(_timestamp);
990 transaction
991 .contexts
992 .insert("trace".into(), inner.context.clone().into());
993
994 Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction));
995 let opts = client.options();
996 transaction.release.clone_from(&opts.release);
997 transaction.environment.clone_from(&opts.environment);
998 transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone()));
999 transaction.server_name.clone_from(&opts.server_name);
1000
1001 let mut dsc = protocol::DynamicSamplingContext::new()
1002 .with_trace_id(inner.context.trace_id)
1003 .with_sample_rate(sample_rate)
1004 .with_sampled(true);
1005 if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) {
1006 dsc = dsc.with_public_key(public_key.to_owned());
1007 }
1008
1009 drop(inner);
1010
1011 let mut envelope = protocol::Envelope::new().with_headers(
1012 protocol::EnvelopeHeaders::new().with_trace(dsc)
1013 );
1014 envelope.add_item(transaction);
1015
1016 client.send_envelope(envelope);
1017 },
1018 FinishAction::Discard => {
1019 client.record_lost_data(&transaction, ClientReportReason::SampleRate);
1020 },
1021 FinishAction::Ignore => (),
1022 }
1023 }
1024 }}
1025 }
1026
1027 pub fn finish(self) {
1032 self.finish_with_timestamp(SystemTime::now());
1033 }
1034
1035 #[must_use = "a span must be explicitly closed via `finish()`"]
1039 pub fn start_child(&self, op: &str, description: &str) -> Span {
1040 let inner = self.inner.lock().unwrap();
1041 let span = protocol::Span {
1042 trace_id: inner.context.trace_id,
1043 parent_span_id: Some(inner.context.span_id),
1044 op: Some(op.into()),
1045 description: if description.is_empty() {
1046 None
1047 } else {
1048 Some(description.into())
1049 },
1050 ..Default::default()
1051 };
1052 Span {
1053 transaction: Arc::clone(&self.inner),
1054 tracing_state: inner.tracing_state,
1055 span: Arc::new(Mutex::new(span)),
1056 }
1057 }
1058
1059 #[must_use = "a span must be explicitly closed via `finish()`"]
1063 pub fn start_child_with_details(
1064 &self,
1065 op: &str,
1066 description: &str,
1067 id: SpanId,
1068 timestamp: SystemTime,
1069 ) -> Span {
1070 let inner = self.inner.lock().unwrap();
1071 let span = protocol::Span {
1072 trace_id: inner.context.trace_id,
1073 parent_span_id: Some(inner.context.span_id),
1074 op: Some(op.into()),
1075 description: if description.is_empty() {
1076 None
1077 } else {
1078 Some(description.into())
1079 },
1080 span_id: id,
1081 start_timestamp: timestamp,
1082 ..Default::default()
1083 };
1084 Span {
1085 transaction: Arc::clone(&self.inner),
1086 tracing_state: inner.tracing_state,
1087 span: Arc::new(Mutex::new(span)),
1088 }
1089 }
1090}
1091
1092impl PartialEq for Transaction {
1093 fn eq(&self, other: &Self) -> bool {
1094 Arc::ptr_eq(&self.inner, &other.inner)
1095 }
1096}
1097
1098pub struct Data<'a>(MutexGuard<'a, protocol::Span>);
1100
1101impl Data<'_> {
1102 pub fn set_data(&mut self, key: String, value: protocol::Value) {
1104 self.0.data.insert(key, value);
1105 }
1106
1107 pub fn set_tag(&mut self, key: String, value: String) {
1109 self.0.tags.insert(key, value);
1110 }
1111}
1112
1113impl Deref for Data<'_> {
1114 type Target = BTreeMap<String, protocol::Value>;
1115
1116 fn deref(&self) -> &Self::Target {
1117 &self.0.data
1118 }
1119}
1120
1121impl DerefMut for Data<'_> {
1122 fn deref_mut(&mut self) -> &mut Self::Target {
1123 &mut self.0.data
1124 }
1125}
1126
1127#[derive(Clone, Debug)]
1132pub struct Span {
1133 pub(crate) transaction: TransactionArc,
1134 tracing_state: TracingState,
1135 span: SpanArc,
1136}
1137
1138type SpanArc = Arc<Mutex<protocol::Span>>;
1139
1140impl Span {
1141 pub fn set_data(&self, key: &str, value: protocol::Value) {
1143 let mut span = self.span.lock().unwrap();
1144 span.data.insert(key.into(), value);
1145 }
1146
1147 pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
1149 let mut span = self.span.lock().unwrap();
1150 span.tags.insert(key.into(), value.to_string());
1151 }
1152
1153 pub fn data(&self) -> Data<'_> {
1165 Data(self.span.lock().unwrap())
1166 }
1167
1168 pub fn get_trace_context(&self) -> protocol::TraceContext {
1172 let transaction = self.transaction.lock().unwrap();
1173 transaction.context.clone()
1174 }
1175
1176 pub fn get_span_id(&self) -> protocol::SpanId {
1178 let span = self.span.lock().unwrap();
1179 span.span_id
1180 }
1181
1182 pub fn get_status(&self) -> Option<protocol::SpanStatus> {
1184 let span = self.span.lock().unwrap();
1185 span.status
1186 }
1187
1188 pub fn set_status(&self, status: protocol::SpanStatus) {
1190 let mut span = self.span.lock().unwrap();
1191 span.status = Some(status);
1192 }
1193
1194 pub fn set_op(&self, op: &str) {
1196 let mut span = self.span.lock().unwrap();
1197 span.op = Some(op.to_string());
1198 }
1199
1200 pub fn set_name(&self, name: &str) {
1202 let mut span = self.span.lock().unwrap();
1203 span.description = Some(name.to_string());
1204 }
1205
1206 pub fn set_request(&self, request: protocol::Request) {
1208 let mut span = self.span.lock().unwrap();
1209 if let Some(method) = request.method {
1211 span.data.insert("method".into(), method.into());
1212 }
1213 if let Some(url) = request.url {
1214 span.data.insert("url".into(), url.to_string().into());
1215 }
1216 if let Some(data) = request.data {
1217 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&data) {
1218 span.data.insert("data".into(), data);
1219 } else {
1220 span.data.insert("data".into(), data.into());
1221 }
1222 }
1223 if let Some(query_string) = request.query_string {
1224 span.data.insert("query_string".into(), query_string.into());
1225 }
1226 if let Some(cookies) = request.cookies {
1227 span.data.insert("cookies".into(), cookies.into());
1228 }
1229 if !request.headers.is_empty() {
1230 if let Ok(headers) = serde_json::to_value(request.headers) {
1231 span.data.insert("headers".into(), headers);
1232 }
1233 }
1234 if !request.env.is_empty() {
1235 if let Ok(env) = serde_json::to_value(request.env) {
1236 span.data.insert("env".into(), env);
1237 }
1238 }
1239 }
1240
1241 pub fn iter_headers(&self) -> TraceHeadersIter {
1245 let span = self.span.lock().unwrap();
1246 let trace = TracePropagationContext::new(span.trace_id, span.span_id)
1247 .with_maybe_sampled(self.tracing_state.trace_sampled());
1248
1249 TraceHeadersIter {
1250 sentry_trace: Some(trace.sentry_trace_header()),
1251 }
1252 }
1253
1254 #[deprecated = "the returned value may not accurately represent the sampling decision"]
1266 pub fn is_sampled(&self) -> bool {
1267 #[cfg(feature = "client")]
1271 {
1272 matches!(
1273 self.tracing_state.finish_action(),
1274 FinishAction::Send { .. }
1275 )
1276 }
1277
1278 #[cfg(not(feature = "client"))]
1279 false
1280 }
1281
1282 pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
1287 with_client_impl! {{
1288 let mut span = self.span.lock().unwrap();
1289 if span.timestamp.is_some() {
1290 return;
1292 }
1293 span.finish_with_timestamp(_timestamp);
1294 let mut inner = self.transaction.lock().unwrap();
1295 if matches!(inner.tracing_state.finish_action(), FinishAction::Ignore) {
1297 return;
1298 }
1299 if let Some(transaction) = inner.transaction.as_mut() {
1300 if transaction.spans.len() <= MAX_SPANS {
1301 transaction.spans.push(span.clone());
1302 } else if let Some(client) = inner.client.as_ref() {
1303 client.record_lost_data(&*span, ClientReportReason::BufferOverflow);
1304 }
1305 }
1306 }}
1307 }
1308
1309 pub fn finish(self) {
1314 self.finish_with_timestamp(SystemTime::now());
1315 }
1316
1317 #[must_use = "a span must be explicitly closed via `finish()`"]
1321 pub fn start_child(&self, op: &str, description: &str) -> Span {
1322 let span = self.span.lock().unwrap();
1323 let span = protocol::Span {
1324 trace_id: span.trace_id,
1325 parent_span_id: Some(span.span_id),
1326 op: Some(op.into()),
1327 description: if description.is_empty() {
1328 None
1329 } else {
1330 Some(description.into())
1331 },
1332 ..Default::default()
1333 };
1334 Span {
1335 transaction: self.transaction.clone(),
1336 tracing_state: self.tracing_state,
1337 span: Arc::new(Mutex::new(span)),
1338 }
1339 }
1340
1341 #[must_use = "a span must be explicitly closed via `finish()`"]
1345 fn start_child_with_details(
1346 &self,
1347 op: &str,
1348 description: &str,
1349 id: SpanId,
1350 timestamp: SystemTime,
1351 ) -> Span {
1352 let span = self.span.lock().unwrap();
1353 let span = protocol::Span {
1354 trace_id: span.trace_id,
1355 parent_span_id: Some(span.span_id),
1356 op: Some(op.into()),
1357 description: if description.is_empty() {
1358 None
1359 } else {
1360 Some(description.into())
1361 },
1362 span_id: id,
1363 start_timestamp: timestamp,
1364 ..Default::default()
1365 };
1366 Span {
1367 transaction: self.transaction.clone(),
1368 tracing_state: self.tracing_state,
1369 span: Arc::new(Mutex::new(span)),
1370 }
1371 }
1372}
1373
1374impl PartialEq for Span {
1375 fn eq(&self, other: &Self) -> bool {
1376 Arc::ptr_eq(&self.span, &other.span)
1377 }
1378}
1379
1380pub type TraceHeader = (&'static str, String);
1382
1383pub struct TraceHeadersIter {
1388 sentry_trace: Option<String>,
1389}
1390
1391impl TraceHeadersIter {
1392 #[cfg(feature = "client")]
1393 pub(crate) fn new(sentry_trace: String) -> Self {
1394 Self {
1395 sentry_trace: Some(sentry_trace),
1396 }
1397 }
1398}
1399
1400impl Iterator for TraceHeadersIter {
1401 type Item = (&'static str, String);
1402
1403 fn next(&mut self) -> Option<Self::Item> {
1404 self.sentry_trace.take().map(|st| ("sentry-trace", st))
1405 }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410 use std::sync::Arc;
1411
1412 use super::*;
1413
1414 #[test]
1415 fn disabled_forwards_trace_id() {
1416 let headers = [(
1417 "SenTrY-TRAce",
1418 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1419 )];
1420 let ctx = TransactionContext::continue_from_headers("noop", "noop", headers);
1421 let trx = start_transaction(ctx);
1422
1423 let span = trx.start_child("noop", "noop");
1424
1425 let header = span.iter_headers().next().unwrap().1;
1426 let parsed =
1427 TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();
1428
1429 assert_eq!(
1430 &parsed.trace_id.to_string(),
1431 "09e04486820349518ac7b5d2adbf6ba5"
1432 );
1433 assert_eq!(parsed.sampled, Some(true));
1434 }
1435
1436 #[test]
1437 fn transaction_context_public_getters() {
1438 let mut ctx = TransactionContext::new("test-name", "test-operation");
1439 assert_eq!(ctx.name(), "test-name");
1440 assert_eq!(ctx.operation(), "test-operation");
1441 assert_eq!(ctx.sampled(), None);
1442
1443 ctx.set_sampled(true);
1444 assert_eq!(ctx.sampled(), Some(true));
1445 }
1446
1447 #[test]
1448 fn continue_from_headers_stores_incoming_org_id() {
1449 let ctx = TransactionContext::continue_from_headers(
1450 "noop",
1451 "noop",
1452 [
1453 (
1454 "sentry-trace",
1455 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
1456 ),
1457 ("baggage", "sentry-org_id=123"),
1458 ],
1459 );
1460
1461 assert_eq!(
1462 ctx.incoming_trace.map(|incoming| incoming.org_id),
1463 Some(Some("123".parse().unwrap()))
1464 );
1465 }
1466
1467 #[test]
1468 fn continue_from_headers_does_not_keep_org_id_without_sentry_trace() {
1469 let ctx = TransactionContext::continue_from_headers(
1470 "noop",
1471 "noop",
1472 [("baggage", "sentry-org_id=123")],
1473 );
1474
1475 assert!(ctx.incoming_trace.is_none());
1476 assert_eq!(ctx.parent_span_id, None);
1477 }
1478
1479 #[cfg(feature = "client")]
1480 #[test]
1481 fn compute_transaction_sample_rate() {
1482 let ctx = TransactionContext::new("noop", "noop");
1483 assert_eq!(
1484 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1485 Some(0.3)
1486 );
1487 assert_eq!(
1488 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.7), &ctx),
1489 Some(0.7)
1490 );
1491
1492 let mut ctx = TransactionContext::new("noop", "noop");
1493 ctx.set_sampled(true);
1494 assert_eq!(
1495 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1496 Some(1.0)
1497 );
1498 ctx.set_sampled(false);
1499 assert_eq!(
1500 transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1501 Some(0.0)
1502 );
1503
1504 let ctx = TransactionContext::new("noop", "noop");
1505 assert_eq!(
1506 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1507 None
1508 );
1509 let mut ctx = TransactionContext::new("noop", "noop");
1510 ctx.set_sampled(true);
1511 assert_eq!(
1512 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1513 None
1514 );
1515 ctx.set_sampled(false);
1516 assert_eq!(
1517 transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
1518 None
1519 );
1520
1521 let mut ctx = TransactionContext::new("noop", "noop");
1524 let sampler = |_: &TransactionContext| 0.7_f32;
1525 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1526 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1527 ctx.set_sampled(false);
1528 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1529
1530 let sampler = |ctx: &TransactionContext| match ctx.sampled() {
1531 Some(true) => 0.8_f32,
1532 Some(false) => 0.4_f32,
1533 None => 0.6_f32,
1534 };
1535 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1536 ctx.set_sampled(true);
1537 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.8));
1538 ctx.set_sampled(None);
1539 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.6));
1540
1541 let sampler = |ctx: &TransactionContext| {
1542 if ctx.name() == "must-name" || ctx.operation() == "must-operation" {
1543 return 1.0;
1544 }
1545
1546 if let Some(custom) = ctx.custom() {
1547 if let Some(rate) = custom.get("rate") {
1548 if let Some(rate) = rate.as_f64() {
1549 return rate as f32;
1550 }
1551 }
1552 }
1553
1554 0.1
1555 };
1556 let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
1557 let ctx = TransactionContext::new("noop", "must-operation");
1558 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1559 let ctx = TransactionContext::new("must-name", "noop");
1560 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(1.0));
1561 let mut ctx = TransactionContext::new("noop", "noop");
1562 ctx.custom_insert("rate".to_owned(), serde_json::json!(0.7));
1563 assert_eq!(transaction_sample_rate(&strategy, &ctx), Some(0.7));
1564 }
1565}