use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::SystemTime;
#[cfg(feature = "client")]
use sentry_types::protocol::v7::client_report::Reason as ClientReportReason;
#[cfg(feature = "client")]
use sentry_types::protocol::v7::OrganizationId;
use sentry_types::protocol::v7::SpanId;
#[cfg(feature = "client")]
use crate::clientoptions::TracesSamplingStrategy;
use crate::{protocol, Hub};
#[cfg(feature = "client")]
use crate::Client;
#[expect(deprecated, reason = "backwards-compatibility re-export")]
pub use self::headers::{parse_sentry_trace_header as parse_headers, SentryTrace};
pub use self::headers::{HeaderParseError, TracePropagationContext};
mod headers;
#[cfg(feature = "client")]
const MAX_SPANS: usize = 1_000;
pub fn start_transaction(ctx: TransactionContext) -> Transaction {
#[cfg(feature = "client")]
{
let client = Hub::with_active(|hub| hub.client());
Transaction::new(client, ctx)
}
#[cfg(not(feature = "client"))]
{
Transaction::new_noop(ctx)
}
}
pub fn start_transaction_with_timestamp(
ctx: TransactionContext,
timestamp: SystemTime,
) -> Transaction {
let transaction = start_transaction(ctx);
if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
tx.start_timestamp = timestamp;
}
transaction
}
impl Hub {
pub fn start_transaction(&self, ctx: TransactionContext) -> Transaction {
#[cfg(feature = "client")]
{
Transaction::new(self.client(), ctx)
}
#[cfg(not(feature = "client"))]
{
Transaction::new_noop(ctx)
}
}
pub fn start_transaction_with_timestamp(
&self,
ctx: TransactionContext,
timestamp: SystemTime,
) -> Transaction {
let transaction = start_transaction(ctx);
if let Some(tx) = transaction.inner.lock().unwrap().transaction.as_mut() {
tx.start_timestamp = timestamp;
}
transaction
}
}
pub type CustomTransactionContext = serde_json::Map<String, serde_json::Value>;
#[cfg(feature = "client")]
#[derive(Debug, Clone, Copy)]
struct IncomingTrace {
org_id: Option<OrganizationId>,
}
#[derive(Debug, Clone)]
pub struct TransactionContext {
#[cfg_attr(not(feature = "client"), allow(dead_code))]
name: String,
op: String,
trace_id: protocol::TraceId,
parent_span_id: Option<protocol::SpanId>,
span_id: protocol::SpanId,
sampled: Option<bool>,
#[cfg(feature = "client")]
incoming_trace: Option<IncomingTrace>,
custom: Option<CustomTransactionContext>,
}
impl TransactionContext {
#[must_use = "this must be used with `start_transaction`"]
pub fn new(name: &str, op: &str) -> Self {
Self::new_with_trace_id(name, op, protocol::TraceId::default())
}
#[must_use = "this must be used with `start_transaction`"]
pub fn new_with_trace_id(name: &str, op: &str, trace_id: protocol::TraceId) -> Self {
Self {
name: name.into(),
op: op.into(),
trace_id,
parent_span_id: None,
span_id: Default::default(),
sampled: None,
#[cfg(feature = "client")]
incoming_trace: None,
custom: None,
}
}
#[must_use = "this must be used with `start_transaction`"]
pub fn new_with_details(
name: &str,
op: &str,
trace_id: protocol::TraceId,
span_id: Option<protocol::SpanId>,
parent_span_id: Option<protocol::SpanId>,
) -> Self {
let mut slf = Self::new_with_trace_id(name, op, trace_id);
if let Some(span_id) = span_id {
slf.span_id = span_id;
}
slf.parent_span_id = parent_span_id;
slf
}
#[must_use = "this must be used with `start_transaction`"]
pub fn continue_from_headers<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(
name: &str,
op: &str,
headers: I,
) -> Self {
TracePropagationContext::try_from_headers(headers)
.map(|context| Self::continue_from_trace_propagation_context(name, op, &context, None))
.unwrap_or_else(|_| Self {
name: name.into(),
op: op.into(),
trace_id: Default::default(),
parent_span_id: None,
span_id: Default::default(),
sampled: None,
#[cfg(feature = "client")]
incoming_trace: None,
custom: None,
})
}
#[deprecated = "use `TransactionContext::continue_from_trace_propagation_context` instead"]
#[expect(deprecated, reason = "backwards-compatible method")]
pub fn continue_from_sentry_trace(
name: &str,
op: &str,
sentry_trace: &SentryTrace,
span_id: Option<SpanId>,
) -> Self {
let context = (*sentry_trace).into();
Self::continue_from_trace_propagation_context(name, op, &context, span_id)
}
pub fn continue_from_trace_propagation_context(
name: &str,
op: &str,
context: &TracePropagationContext,
span_id: Option<SpanId>,
) -> Self {
let &TracePropagationContext {
trace_id,
span_id: context_span_id,
sampled,
#[cfg(feature = "client")]
org_id,
} = context;
Self {
name: name.into(),
op: op.into(),
trace_id,
parent_span_id: Some(context_span_id),
sampled,
#[cfg(feature = "client")]
incoming_trace: Some(IncomingTrace { org_id }),
span_id: span_id.unwrap_or_default(),
custom: None,
}
}
pub fn continue_from_span(name: &str, op: &str, span: Option<TransactionOrSpan>) -> Self {
let span = match span {
Some(span) => span,
None => return Self::new(name, op),
};
let (trace_id, parent_span_id, sampled) = match span {
TransactionOrSpan::Transaction(transaction) => {
let inner = transaction.inner.lock().unwrap();
(
inner.context.trace_id,
inner.context.span_id,
Some(inner.sampled),
)
}
TransactionOrSpan::Span(span) => {
let sampled = span.sampled;
let span = span.span.lock().unwrap();
(span.trace_id, span.span_id, Some(sampled))
}
};
Self {
name: name.into(),
op: op.into(),
trace_id,
parent_span_id: Some(parent_span_id),
span_id: protocol::SpanId::default(),
sampled,
#[cfg(feature = "client")]
incoming_trace: None,
custom: None,
}
}
pub fn set_sampled(&mut self, sampled: impl Into<Option<bool>>) {
self.sampled = sampled.into();
}
pub fn sampled(&self) -> Option<bool> {
self.sampled
}
pub fn name(&self) -> &str {
&self.name
}
pub fn operation(&self) -> &str {
&self.op
}
pub fn trace_id(&self) -> protocol::TraceId {
self.trace_id
}
pub fn span_id(&self) -> protocol::SpanId {
self.span_id
}
pub fn custom(&self) -> Option<&CustomTransactionContext> {
self.custom.as_ref()
}
pub fn custom_mut(&mut self) -> &mut Option<CustomTransactionContext> {
&mut self.custom
}
pub fn custom_insert(
&mut self,
key: String,
value: serde_json::Value,
) -> Option<serde_json::Value> {
let mut custom = None;
std::mem::swap(&mut self.custom, &mut custom);
let mut custom = custom.unwrap_or_default();
let existing_value = custom.insert(key, value);
self.custom = Some(custom);
existing_value
}
#[must_use]
pub fn builder(name: &str, op: &str) -> TransactionContextBuilder {
TransactionContextBuilder {
ctx: TransactionContext::new(name, op),
}
}
#[cfg(feature = "client")]
fn reject_incoming_trace(&mut self) {
(
self.trace_id,
self.parent_span_id,
self.sampled,
self.incoming_trace,
) = Default::default();
}
}
pub struct TransactionContextBuilder {
ctx: TransactionContext,
}
impl TransactionContextBuilder {
#[must_use]
pub fn with_name(mut self, name: String) -> Self {
self.ctx.name = name;
self
}
#[must_use]
pub fn with_op(mut self, op: String) -> Self {
self.ctx.op = op;
self
}
#[must_use]
pub fn with_trace_id(mut self, trace_id: protocol::TraceId) -> Self {
self.ctx.trace_id = trace_id;
self
}
#[must_use]
pub fn with_parent_span_id(mut self, parent_span_id: Option<protocol::SpanId>) -> Self {
self.ctx.parent_span_id = parent_span_id;
self
}
#[must_use]
pub fn with_span_id(mut self, span_id: protocol::SpanId) -> Self {
self.ctx.span_id = span_id;
self
}
#[must_use]
pub fn with_sampled(mut self, sampled: Option<bool>) -> Self {
self.ctx.sampled = sampled;
self
}
#[must_use]
pub fn with_custom(mut self, key: String, value: serde_json::Value) -> Self {
self.ctx.custom_insert(key, value);
self
}
pub fn finish(self) -> TransactionContext {
self.ctx
}
}
pub type TracesSampler = dyn Fn(&TransactionContext) -> f32 + Send + Sync;
#[derive(Clone, Debug, PartialEq)]
pub enum TransactionOrSpan {
Transaction(Transaction),
Span(Span),
}
impl From<Transaction> for TransactionOrSpan {
fn from(transaction: Transaction) -> Self {
Self::Transaction(transaction)
}
}
impl From<Span> for TransactionOrSpan {
fn from(span: Span) -> Self {
Self::Span(span)
}
}
impl TransactionOrSpan {
pub fn set_data(&self, key: &str, value: protocol::Value) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.set_data(key, value),
TransactionOrSpan::Span(span) => span.set_data(key, value),
}
}
pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.set_tag(key, value),
TransactionOrSpan::Span(span) => span.set_tag(key, value),
}
}
pub fn get_trace_context(&self) -> protocol::TraceContext {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.get_trace_context(),
TransactionOrSpan::Span(span) => span.get_trace_context(),
}
}
pub fn get_status(&self) -> Option<protocol::SpanStatus> {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.get_status(),
TransactionOrSpan::Span(span) => span.get_status(),
}
}
pub fn set_status(&self, status: protocol::SpanStatus) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.set_status(status),
TransactionOrSpan::Span(span) => span.set_status(status),
}
}
pub fn set_op(&self, op: &str) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.set_op(op),
TransactionOrSpan::Span(span) => span.set_op(op),
}
}
pub fn set_name(&self, name: &str) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.set_name(name),
TransactionOrSpan::Span(span) => span.set_name(name),
}
}
pub fn set_request(&self, request: protocol::Request) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.set_request(request),
TransactionOrSpan::Span(span) => span.set_request(request),
}
}
pub fn iter_headers(&self) -> TraceHeadersIter {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.iter_headers(),
TransactionOrSpan::Span(span) => span.iter_headers(),
}
}
pub fn is_sampled(&self) -> bool {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.is_sampled(),
TransactionOrSpan::Span(span) => span.is_sampled(),
}
}
#[must_use = "a span must be explicitly closed via `finish()`"]
pub fn start_child(&self, op: &str, description: &str) -> Span {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.start_child(op, description),
TransactionOrSpan::Span(span) => span.start_child(op, description),
}
}
#[must_use = "a span must be explicitly closed via `finish()`"]
pub fn start_child_with_details(
&self,
op: &str,
description: &str,
id: SpanId,
timestamp: SystemTime,
) -> Span {
match self {
TransactionOrSpan::Transaction(transaction) => {
transaction.start_child_with_details(op, description, id, timestamp)
}
TransactionOrSpan::Span(span) => {
span.start_child_with_details(op, description, id, timestamp)
}
}
}
#[cfg(feature = "client")]
pub(crate) fn apply_to_event(&self, event: &mut protocol::Event<'_>) {
if event.contexts.contains_key("trace") {
return;
}
let context = match self {
TransactionOrSpan::Transaction(transaction) => {
transaction.inner.lock().unwrap().context.clone()
}
TransactionOrSpan::Span(span) => {
let span = span.span.lock().unwrap();
protocol::TraceContext {
span_id: span.span_id,
trace_id: span.trace_id,
..Default::default()
}
}
};
event.contexts.insert("trace".into(), context.into());
}
pub fn finish_with_timestamp(self, timestamp: SystemTime) {
match self {
TransactionOrSpan::Transaction(transaction) => {
transaction.finish_with_timestamp(timestamp)
}
TransactionOrSpan::Span(span) => span.finish_with_timestamp(timestamp),
}
}
pub fn finish(self) {
match self {
TransactionOrSpan::Transaction(transaction) => transaction.finish(),
TransactionOrSpan::Span(span) => span.finish(),
}
}
}
#[derive(Debug)]
pub(crate) struct TransactionInner {
#[cfg(feature = "client")]
client: Option<Arc<Client>>,
sampled: bool,
pub(crate) context: protocol::TraceContext,
pub(crate) transaction: Option<protocol::Transaction<'static>>,
}
type TransactionArc = Arc<Mutex<TransactionInner>>;
#[cfg(feature = "client")]
fn transaction_sample_rate(
traces_sampling_strategy: &TracesSamplingStrategy,
ctx: &TransactionContext,
) -> f32 {
match traces_sampling_strategy {
&TracesSamplingStrategy::FixedRate(rate) => ctx.sampled.map_or(rate, f32::from),
TracesSamplingStrategy::Function(traces_sampler) => traces_sampler(ctx),
TracesSamplingStrategy::Disabled => 0.0,
}
}
#[cfg(feature = "client")]
fn should_continue_trace(
incoming: Option<OrganizationId>,
sdk: Option<OrganizationId>,
strict: bool,
) -> bool {
match (incoming, sdk) {
(Some(incoming), Some(sdk)) => incoming == sdk,
(Some(_), None) | (None, Some(_)) => !strict,
(None, None) => true,
}
}
#[cfg(feature = "client")]
impl Client {
fn determine_sampling_decision(&self, ctx: &TransactionContext) -> (bool, f32) {
let client_options = self.options();
let sample_rate = transaction_sample_rate(&client_options.traces_sampling_strategy, ctx);
let sampled = self.sample_should_send(sample_rate);
(sampled, sample_rate)
}
}
#[cfg(feature = "client")]
#[derive(Clone, Debug)]
struct TransactionMetadata {
sample_rate: f32,
}
#[derive(Clone, Debug)]
pub struct Transaction {
pub(crate) inner: TransactionArc,
#[cfg(feature = "client")]
metadata: TransactionMetadata,
}
pub struct TransactionData<'a>(MutexGuard<'a, TransactionInner>);
impl<'a> TransactionData<'a> {
pub fn iter(&self) -> Box<dyn Iterator<Item = (&String, &protocol::Value)> + '_> {
if self.0.transaction.is_some() {
Box::new(self.0.context.data.iter())
} else {
Box::new(std::iter::empty())
}
}
pub fn set_data(&mut self, key: Cow<'a, str>, value: protocol::Value) {
if self.0.transaction.is_some() {
self.0.context.data.insert(key.into(), value);
}
}
pub fn set_tag(&mut self, key: Cow<'_, str>, value: String) {
if let Some(transaction) = self.0.transaction.as_mut() {
transaction.tags.insert(key.into(), value);
}
}
}
impl Transaction {
#[cfg(feature = "client")]
fn new(client: Option<Arc<Client>>, mut ctx: TransactionContext) -> Self {
let ((sampled, sample_rate), transaction) = match client.as_ref() {
Some(client) => {
let options = client.options();
let sdk_org_id = options.org_id.or_else(|| options.dsn.as_ref()?.org_id());
if ctx.incoming_trace.is_some_and(
|IncomingTrace {
org_id: incoming_org_id,
}| {
!should_continue_trace(
incoming_org_id,
sdk_org_id,
options.strict_trace_continuation,
)
},
) {
ctx.reject_incoming_trace();
}
(
client.determine_sampling_decision(&ctx),
Some(protocol::Transaction {
name: Some(ctx.name),
..Default::default()
}),
)
}
None => (
(
ctx.sampled.unwrap_or(false),
ctx.sampled.map_or(0.0, f32::from),
),
None,
),
};
let context = protocol::TraceContext {
trace_id: ctx.trace_id,
parent_span_id: ctx.parent_span_id,
span_id: ctx.span_id,
op: Some(ctx.op),
..Default::default()
};
Self {
inner: Arc::new(Mutex::new(TransactionInner {
client,
sampled,
context,
transaction,
})),
metadata: TransactionMetadata { sample_rate },
}
}
#[cfg(not(feature = "client"))]
fn new_noop(ctx: TransactionContext) -> Self {
let context = protocol::TraceContext {
trace_id: ctx.trace_id,
parent_span_id: ctx.parent_span_id,
op: Some(ctx.op),
..Default::default()
};
let sampled = ctx.sampled.unwrap_or(false);
Self {
inner: Arc::new(Mutex::new(TransactionInner {
sampled,
context,
transaction: None,
})),
}
}
pub fn set_data(&self, key: &str, value: protocol::Value) {
let mut inner = self.inner.lock().unwrap();
if inner.transaction.is_some() {
inner.context.data.insert(key.into(), value);
}
}
pub fn set_extra(&self, key: &str, value: protocol::Value) {
let mut inner = self.inner.lock().unwrap();
if let Some(transaction) = inner.transaction.as_mut() {
transaction.extra.insert(key.into(), value);
}
}
pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
let mut inner = self.inner.lock().unwrap();
if let Some(transaction) = inner.transaction.as_mut() {
transaction.tags.insert(key.into(), value.to_string());
}
}
pub fn data(&self) -> TransactionData<'_> {
TransactionData(self.inner.lock().unwrap())
}
pub fn get_trace_context(&self) -> protocol::TraceContext {
let inner = self.inner.lock().unwrap();
inner.context.clone()
}
pub fn get_status(&self) -> Option<protocol::SpanStatus> {
let inner = self.inner.lock().unwrap();
inner.context.status
}
pub fn set_status(&self, status: protocol::SpanStatus) {
let mut inner = self.inner.lock().unwrap();
inner.context.status = Some(status);
}
pub fn set_op(&self, op: &str) {
let mut inner = self.inner.lock().unwrap();
inner.context.op = Some(op.to_string());
}
pub fn set_name(&self, name: &str) {
let mut inner = self.inner.lock().unwrap();
if let Some(transaction) = inner.transaction.as_mut() {
transaction.name = Some(name.to_string());
}
}
pub fn set_request(&self, request: protocol::Request) {
let mut inner = self.inner.lock().unwrap();
if let Some(transaction) = inner.transaction.as_mut() {
transaction.request = Some(request);
}
}
pub fn set_origin(&self, origin: &str) {
let mut inner = self.inner.lock().unwrap();
inner.context.origin = Some(origin.to_owned());
}
pub fn iter_headers(&self) -> TraceHeadersIter {
let inner = self.inner.lock().unwrap();
let trace = TracePropagationContext::new(inner.context.trace_id, inner.context.span_id)
.with_sampled(inner.sampled);
TraceHeadersIter {
sentry_trace: Some(trace.sentry_trace_header()),
}
}
pub fn is_sampled(&self) -> bool {
self.inner.lock().unwrap().sampled
}
pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
with_client_impl! {{
let mut inner = self.inner.lock().unwrap();
if !inner.sampled {
if let Some(transaction) = inner.transaction.take() {
if let Some(client) = inner.client.as_ref() {
client.record_lost_data(&transaction, ClientReportReason::SampleRate);
}
}
return;
}
if let Some(mut transaction) = inner.transaction.take() {
if let Some(client) = inner.client.take() {
transaction.finish_with_timestamp(_timestamp);
transaction
.contexts
.insert("trace".into(), inner.context.clone().into());
Hub::current().with_current_scope(|scope| scope.apply_to_transaction(&mut transaction));
let opts = client.options();
transaction.release.clone_from(&opts.release);
transaction.environment.clone_from(&opts.environment);
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().with_headers(
protocol::EnvelopeHeaders::new().with_trace(dsc)
);
envelope.add_item(transaction);
client.send_envelope(envelope)
}
}
}}
}
pub fn finish(self) {
self.finish_with_timestamp(SystemTime::now());
}
#[must_use = "a span must be explicitly closed via `finish()`"]
pub fn start_child(&self, op: &str, description: &str) -> Span {
let inner = self.inner.lock().unwrap();
let span = protocol::Span {
trace_id: inner.context.trace_id,
parent_span_id: Some(inner.context.span_id),
op: Some(op.into()),
description: if description.is_empty() {
None
} else {
Some(description.into())
},
..Default::default()
};
Span {
transaction: Arc::clone(&self.inner),
sampled: inner.sampled,
span: Arc::new(Mutex::new(span)),
}
}
#[must_use = "a span must be explicitly closed via `finish()`"]
pub fn start_child_with_details(
&self,
op: &str,
description: &str,
id: SpanId,
timestamp: SystemTime,
) -> Span {
let inner = self.inner.lock().unwrap();
let span = protocol::Span {
trace_id: inner.context.trace_id,
parent_span_id: Some(inner.context.span_id),
op: Some(op.into()),
description: if description.is_empty() {
None
} else {
Some(description.into())
},
span_id: id,
start_timestamp: timestamp,
..Default::default()
};
Span {
transaction: Arc::clone(&self.inner),
sampled: inner.sampled,
span: Arc::new(Mutex::new(span)),
}
}
}
impl PartialEq for Transaction {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
}
pub struct Data<'a>(MutexGuard<'a, protocol::Span>);
impl Data<'_> {
pub fn set_data(&mut self, key: String, value: protocol::Value) {
self.0.data.insert(key, value);
}
pub fn set_tag(&mut self, key: String, value: String) {
self.0.tags.insert(key, value);
}
}
impl Deref for Data<'_> {
type Target = BTreeMap<String, protocol::Value>;
fn deref(&self) -> &Self::Target {
&self.0.data
}
}
impl DerefMut for Data<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0.data
}
}
#[derive(Clone, Debug)]
pub struct Span {
pub(crate) transaction: TransactionArc,
sampled: bool,
span: SpanArc,
}
type SpanArc = Arc<Mutex<protocol::Span>>;
impl Span {
pub fn set_data(&self, key: &str, value: protocol::Value) {
let mut span = self.span.lock().unwrap();
span.data.insert(key.into(), value);
}
pub fn set_tag<V: ToString>(&self, key: &str, value: V) {
let mut span = self.span.lock().unwrap();
span.tags.insert(key.into(), value.to_string());
}
pub fn data(&self) -> Data<'_> {
Data(self.span.lock().unwrap())
}
pub fn get_trace_context(&self) -> protocol::TraceContext {
let transaction = self.transaction.lock().unwrap();
transaction.context.clone()
}
pub fn get_span_id(&self) -> protocol::SpanId {
let span = self.span.lock().unwrap();
span.span_id
}
pub fn get_status(&self) -> Option<protocol::SpanStatus> {
let span = self.span.lock().unwrap();
span.status
}
pub fn set_status(&self, status: protocol::SpanStatus) {
let mut span = self.span.lock().unwrap();
span.status = Some(status);
}
pub fn set_op(&self, op: &str) {
let mut span = self.span.lock().unwrap();
span.op = Some(op.to_string());
}
pub fn set_name(&self, name: &str) {
let mut span = self.span.lock().unwrap();
span.description = Some(name.to_string());
}
pub fn set_request(&self, request: protocol::Request) {
let mut span = self.span.lock().unwrap();
if let Some(method) = request.method {
span.data.insert("method".into(), method.into());
}
if let Some(url) = request.url {
span.data.insert("url".into(), url.to_string().into());
}
if let Some(data) = request.data {
if let Ok(data) = serde_json::from_str::<serde_json::Value>(&data) {
span.data.insert("data".into(), data);
} else {
span.data.insert("data".into(), data.into());
}
}
if let Some(query_string) = request.query_string {
span.data.insert("query_string".into(), query_string.into());
}
if let Some(cookies) = request.cookies {
span.data.insert("cookies".into(), cookies.into());
}
if !request.headers.is_empty() {
if let Ok(headers) = serde_json::to_value(request.headers) {
span.data.insert("headers".into(), headers);
}
}
if !request.env.is_empty() {
if let Ok(env) = serde_json::to_value(request.env) {
span.data.insert("env".into(), env);
}
}
}
pub fn iter_headers(&self) -> TraceHeadersIter {
let span = self.span.lock().unwrap();
let trace =
TracePropagationContext::new(span.trace_id, span.span_id).with_sampled(self.sampled);
TraceHeadersIter {
sentry_trace: Some(trace.sentry_trace_header()),
}
}
pub fn is_sampled(&self) -> bool {
self.sampled
}
pub fn finish_with_timestamp(self, _timestamp: SystemTime) {
with_client_impl! {{
let mut span = self.span.lock().unwrap();
if span.timestamp.is_some() {
return;
}
span.finish_with_timestamp(_timestamp);
let mut inner = self.transaction.lock().unwrap();
if let Some(transaction) = inner.transaction.as_mut() {
if transaction.spans.len() <= MAX_SPANS {
transaction.spans.push(span.clone());
} else if let Some(client) = inner.client.as_ref() {
client.record_lost_data(&*span, ClientReportReason::BufferOverflow);
}
}
}}
}
pub fn finish(self) {
self.finish_with_timestamp(SystemTime::now());
}
#[must_use = "a span must be explicitly closed via `finish()`"]
pub fn start_child(&self, op: &str, description: &str) -> Span {
let span = self.span.lock().unwrap();
let span = protocol::Span {
trace_id: span.trace_id,
parent_span_id: Some(span.span_id),
op: Some(op.into()),
description: if description.is_empty() {
None
} else {
Some(description.into())
},
..Default::default()
};
Span {
transaction: self.transaction.clone(),
sampled: self.sampled,
span: Arc::new(Mutex::new(span)),
}
}
#[must_use = "a span must be explicitly closed via `finish()`"]
fn start_child_with_details(
&self,
op: &str,
description: &str,
id: SpanId,
timestamp: SystemTime,
) -> Span {
let span = self.span.lock().unwrap();
let span = protocol::Span {
trace_id: span.trace_id,
parent_span_id: Some(span.span_id),
op: Some(op.into()),
description: if description.is_empty() {
None
} else {
Some(description.into())
},
span_id: id,
start_timestamp: timestamp,
..Default::default()
};
Span {
transaction: self.transaction.clone(),
sampled: self.sampled,
span: Arc::new(Mutex::new(span)),
}
}
}
impl PartialEq for Span {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.span, &other.span)
}
}
pub type TraceHeader = (&'static str, String);
pub struct TraceHeadersIter {
sentry_trace: Option<String>,
}
impl TraceHeadersIter {
#[cfg(feature = "client")]
pub(crate) fn new(sentry_trace: String) -> Self {
Self {
sentry_trace: Some(sentry_trace),
}
}
}
impl Iterator for TraceHeadersIter {
type Item = (&'static str, String);
fn next(&mut self) -> Option<Self::Item> {
self.sentry_trace.take().map(|st| ("sentry-trace", st))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[test]
fn disabled_forwards_trace_id() {
let headers = [(
"SenTrY-TRAce",
"09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
)];
let ctx = TransactionContext::continue_from_headers("noop", "noop", headers);
let trx = start_transaction(ctx);
let span = trx.start_child("noop", "noop");
let header = span.iter_headers().next().unwrap().1;
let parsed =
TracePropagationContext::try_from_headers([("sentry-trace", header.as_str())]).unwrap();
assert_eq!(
&parsed.trace_id.to_string(),
"09e04486820349518ac7b5d2adbf6ba5"
);
assert_eq!(parsed.sampled, Some(true));
}
#[test]
fn transaction_context_public_getters() {
let mut ctx = TransactionContext::new("test-name", "test-operation");
assert_eq!(ctx.name(), "test-name");
assert_eq!(ctx.operation(), "test-operation");
assert_eq!(ctx.sampled(), None);
ctx.set_sampled(true);
assert_eq!(ctx.sampled(), Some(true));
}
#[test]
fn continue_from_headers_stores_incoming_org_id() {
let ctx = TransactionContext::continue_from_headers(
"noop",
"noop",
[
(
"sentry-trace",
"09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
),
("baggage", "sentry-org_id=123"),
],
);
assert_eq!(
ctx.incoming_trace.map(|incoming| incoming.org_id),
Some(Some("123".parse().unwrap()))
);
}
#[test]
fn continue_from_headers_does_not_keep_org_id_without_sentry_trace() {
let ctx = TransactionContext::continue_from_headers(
"noop",
"noop",
[("baggage", "sentry-org_id=123")],
);
assert!(ctx.incoming_trace.is_none());
assert_eq!(ctx.parent_span_id, None);
}
#[cfg(feature = "client")]
#[test]
fn compute_transaction_sample_rate() {
let ctx = TransactionContext::new("noop", "noop");
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
0.3
);
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.7), &ctx),
0.7
);
let mut ctx = TransactionContext::new("noop", "noop");
ctx.set_sampled(true);
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
1.0
);
ctx.set_sampled(false);
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::FixedRate(0.3), &ctx),
0.0
);
let ctx = TransactionContext::new("noop", "noop");
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
0.0
);
let mut ctx = TransactionContext::new("noop", "noop");
ctx.set_sampled(true);
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
0.0
);
ctx.set_sampled(false);
assert_eq!(
transaction_sample_rate(&TracesSamplingStrategy::Disabled, &ctx),
0.0
);
let mut ctx = TransactionContext::new("noop", "noop");
let sampler = |_: &TransactionContext| 0.7_f32;
let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.7);
ctx.set_sampled(false);
assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.7);
let sampler = |ctx: &TransactionContext| match ctx.sampled() {
Some(true) => 0.8_f32,
Some(false) => 0.4_f32,
None => 0.6_f32,
};
let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
ctx.set_sampled(true);
assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.8);
ctx.set_sampled(None);
assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.6);
let sampler = |ctx: &TransactionContext| {
if ctx.name() == "must-name" || ctx.operation() == "must-operation" {
return 1.0;
}
if let Some(custom) = ctx.custom() {
if let Some(rate) = custom.get("rate") {
if let Some(rate) = rate.as_f64() {
return rate as f32;
}
}
}
0.1
};
let strategy = TracesSamplingStrategy::Function(Arc::new(sampler) as Arc<TracesSampler>);
let ctx = TransactionContext::new("noop", "must-operation");
assert_eq!(transaction_sample_rate(&strategy, &ctx), 1.0);
let ctx = TransactionContext::new("must-name", "noop");
assert_eq!(transaction_sample_rate(&strategy, &ctx), 1.0);
let mut ctx = TransactionContext::new("noop", "noop");
ctx.custom_insert("rate".to_owned(), serde_json::json!(0.7));
assert_eq!(transaction_sample_rate(&strategy, &ctx), 0.7);
}
}