diff --git a/CHANGELOG.md b/CHANGELOG.md index c12e34fa..2be06572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - feat(types): add all the missing supported envelope headers ([#867](https://github.com/getsentry/sentry-rust/pull/867)) by @lcian - feat(types): add setters for envelope headers ([#868](https://github.com/getsentry/sentry-rust/pull/868)) by @lcian - It's now possible to set all of the [envelope headers](https://develop.sentry.dev/sdk/data-model/envelopes/#headers) supported by the protocol when constructing envelopes. +- feat(core): add some DSC fields to transaction envelope headers ([#869](https://github.com/getsentry/sentry-rust/pull/869)) by @lcian + - The SDK now sends additional envelope headers with transactions. This should solve some extrapolation issues for span metrics. ### Behavioral changes diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 10c98ac1..18a96d1f 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -594,26 +594,33 @@ fn transaction_sample_rate( ) -> f32 { match (traces_sampler, traces_sample_rate) { (Some(traces_sampler), _) => traces_sampler(ctx), - (None, traces_sample_rate) => ctx - .sampled - .map(|sampled| if sampled { 1.0 } else { 0.0 }) - .unwrap_or(traces_sample_rate), + (None, traces_sample_rate) => ctx.sampled.map(f32::from).unwrap_or(traces_sample_rate), } } /// Determine whether the new transaction should be sampled. #[cfg(feature = "client")] impl Client { - fn is_transaction_sampled(&self, ctx: &TransactionContext) -> bool { + fn determine_sampling_decision(&self, ctx: &TransactionContext) -> (bool, f32) { let client_options = self.options(); - self.sample_should_send(transaction_sample_rate( + let sample_rate = transaction_sample_rate( client_options.traces_sampler.as_deref(), ctx, client_options.traces_sample_rate, - )) + ); + let sampled = self.sample_should_send(sample_rate); + (sampled, sample_rate) } } +/// Some metadata associated with a transaction. +#[cfg(feature = "client")] +#[derive(Clone, Debug)] +struct TransactionMetadata { + /// The sample rate used when making the sampling decision for the associated transaction. + sample_rate: f32, +} + /// A running Performance Monitoring Transaction. /// /// The transaction needs to be explicitly finished via [`Transaction::finish`], @@ -622,6 +629,8 @@ impl Client { #[derive(Clone, Debug)] pub struct Transaction { pub(crate) inner: TransactionArc, + #[cfg(feature = "client")] + metadata: TransactionMetadata, } /// Iterable for a transaction's [data attributes](protocol::TraceContext::data). @@ -660,15 +669,21 @@ impl<'a> TransactionData<'a> { impl Transaction { #[cfg(feature = "client")] fn new(client: Option>, ctx: TransactionContext) -> Self { - let (sampled, transaction) = match client.as_ref() { + let ((sampled, sample_rate), transaction) = match client.as_ref() { Some(client) => ( - client.is_transaction_sampled(&ctx), + client.determine_sampling_decision(&ctx), Some(protocol::Transaction { name: Some(ctx.name), ..Default::default() }), ), - None => (ctx.sampled.unwrap_or(false), None), + None => ( + ( + ctx.sampled.unwrap_or(false), + ctx.sampled.map_or(0.0, f32::from), + ), + None, + ), }; let context = protocol::TraceContext { @@ -686,6 +701,7 @@ impl Transaction { context, transaction, })), + metadata: TransactionMetadata { sample_rate }, } } @@ -820,9 +836,19 @@ impl Transaction { transaction.sdk = Some(std::borrow::Cow::Owned(client.sdk_info.clone())); transaction.server_name.clone_from(&opts.server_name); + let mut dsc = protocol::DynamicSamplingContext::new() + .with_trace_id(inner.context.trace_id) + .with_sample_rate(self.metadata.sample_rate) + .with_sampled(inner.sampled); + if let Some(public_key) = client.dsn().map(|dsn| dsn.public_key()) { + dsc = dsc.with_public_key(public_key.to_owned()); + } + drop(inner); - let mut envelope = protocol::Envelope::new(); + let mut envelope = protocol::Envelope::new().with_headers( + protocol::EnvelopeHeaders::new().with_trace(dsc) + ); envelope.add_item(transaction); client.send_envelope(envelope) diff --git a/sentry/tests/test_basic.rs b/sentry/tests/test_basic.rs index 8f5f4d0b..21cb98ab 100644 --- a/sentry/tests/test_basic.rs +++ b/sentry/tests/test_basic.rs @@ -3,8 +3,10 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use sentry::protocol::{Attachment, Context, EnvelopeItem}; -use sentry::types::Uuid; +use sentry::protocol::{ + Attachment, Context, DynamicSamplingContext, EnvelopeHeaders, EnvelopeItem, +}; +use sentry::types::{Dsn, Uuid}; #[test] fn test_basic_capture_message() { @@ -578,3 +580,39 @@ fn test_basic_capture_log_macro_message_formatted_with_attributes() { _ => panic!("expected item container"), } } + +#[test] +fn test_transaction_envelope_dsc_headers() { + let mut trace_id: Option = None; + let dsn: Option = "http://foo@example.com/42".parse().ok(); + let envelopes = sentry::test::with_captured_envelopes_options( + || { + let transaction_ctx = sentry::TransactionContext::new("name transaction", "op"); + trace_id = Some(transaction_ctx.trace_id()); + let transaction = sentry::start_transaction(transaction_ctx); + sentry::configure_scope(|scope| scope.set_span(Some(transaction.clone().into()))); + transaction.finish(); + }, + sentry::ClientOptions { + dsn: dsn.clone(), + traces_sample_rate: 1.0, + ..Default::default() + }, + ); + + assert!(trace_id.is_some()); + let trace_id = trace_id.unwrap(); + assert_eq!(envelopes.len(), 1); + let envelope = envelopes.into_iter().next().unwrap(); + assert!(envelope.uuid().is_some()); + let uuid = envelope.uuid().copied().unwrap(); + + let expected = EnvelopeHeaders::new().with_event_id(uuid).with_trace( + DynamicSamplingContext::new() + .with_trace_id(trace_id) + .with_public_key(dsn.unwrap().public_key().to_owned()) + .with_sample_rate(1.0) + .with_sampled(true), + ); + assert_eq!(envelope.headers(), &expected); +}