#![cfg(feature = "test")]
use std::sync::Arc;
use sentry_core::protocol::{SpanId, TraceId};
use sentry_core::test::TestTransport;
use sentry_core::{Client, ClientOptions, Hub, Transaction};
struct TraceContinuationScenario {
incoming_trace_id: TraceId,
incoming_parent_span_id: SpanId,
transaction: Transaction,
}
impl TraceContinuationScenario {
fn run(
incoming_org_id: Option<&str>,
sdk_org_id: Option<&str>,
strict_trace_continuation: bool,
) -> Self {
let options = sdk_org_id
.map(|org_id| ClientOptions::new().org_id(org_id.parse().unwrap()))
.unwrap_or_default()
.strict_trace_continuation(strict_trace_continuation)
.traces_sample_rate(0.0);
Self::run_with_options(incoming_org_id, options)
}
fn run_with_options(incoming_org_id: Option<&str>, mut options: ClientOptions) -> Self {
options
.dsn
.get_or_insert_with(|| "https://public@sentry.invalid/1".parse().unwrap());
options
.transport
.get_or_insert_with(|| Arc::new(TestTransport::new()));
let incoming_trace_id = TraceId::default();
let incoming_parent_span_id = SpanId::default();
let sentry_trace = format!("{incoming_trace_id}-{incoming_parent_span_id}-1");
let baggage = incoming_org_id.map(|org_id| format!("sentry-org_id={org_id}"));
let headers = std::iter::once(("sentry-trace", sentry_trace.as_str()))
.chain(baggage.as_deref().map(|baggage| ("baggage", baggage)));
let ctx = sentry_core::TransactionContext::continue_from_headers("noop", "noop", headers);
let hub = Arc::new(Hub::new(
Some(Arc::new(Client::with_options(options))),
Arc::new(Default::default()),
));
let transaction = Hub::run(hub, || sentry_core::start_transaction(ctx));
Self {
incoming_trace_id,
incoming_parent_span_id,
transaction,
}
}
fn assert_continued(&self) {
let context = self.transaction.get_trace_context();
assert_eq!(context.trace_id, self.incoming_trace_id);
assert_eq!(context.parent_span_id, Some(self.incoming_parent_span_id));
assert!(self.transaction.is_sampled());
}
fn assert_rejected(&self) {
let context = self.transaction.get_trace_context();
assert_ne!(context.trace_id, self.incoming_trace_id);
assert_eq!(context.parent_span_id, None);
assert!(!self.transaction.is_sampled());
}
}
#[test]
fn start_transaction_continues_when_no_org_ids_and_not_strict() {
TraceContinuationScenario::run(None, None, false).assert_continued();
}
#[test]
fn start_transaction_continues_when_no_org_ids_and_strict() {
TraceContinuationScenario::run(None, None, true).assert_continued();
}
#[test]
fn start_transaction_continues_when_only_incoming_org_id_and_not_strict() {
TraceContinuationScenario::run(Some("42"), None, false).assert_continued();
}
#[test]
fn start_transaction_rejects_when_only_incoming_org_id_and_strict() {
TraceContinuationScenario::run(Some("42"), None, true).assert_rejected();
}
#[test]
fn start_transaction_continues_when_only_sdk_org_id_and_not_strict() {
TraceContinuationScenario::run(None, Some("42"), false).assert_continued();
}
#[test]
fn start_transaction_rejects_when_only_sdk_org_id_and_strict() {
TraceContinuationScenario::run(None, Some("42"), true).assert_rejected();
}
#[test]
fn start_transaction_continues_when_org_ids_match_and_not_strict() {
TraceContinuationScenario::run(Some("42"), Some("42"), false).assert_continued();
}
#[test]
fn start_transaction_continues_when_org_ids_match_and_strict() {
TraceContinuationScenario::run(Some("42"), Some("42"), true).assert_continued();
}
#[test]
fn start_transaction_rejects_when_org_ids_mismatch_and_not_strict() {
TraceContinuationScenario::run(Some("43"), Some("42"), false).assert_rejected();
}
#[test]
fn start_transaction_rejects_when_org_ids_mismatch_and_strict() {
TraceContinuationScenario::run(Some("43"), Some("42"), true).assert_rejected();
}
#[test]
fn start_transaction_continues_local_span_in_strict_mode() {
let options = ClientOptions::new()
.org_id("42".parse().unwrap())
.strict_trace_continuation(true)
.traces_sample_rate(1.0)
.transport(Arc::new(TestTransport::new()));
let hub = Arc::new(Hub::new(
Some(Arc::new(Client::with_options(options))),
Arc::new(Default::default()),
));
Hub::run(hub, || {
let parent =
sentry_core::start_transaction(sentry_core::TransactionContext::new("parent", "test"));
let parent_context = parent.get_trace_context();
let ctx = sentry_core::TransactionContext::continue_from_span(
"continued",
"test",
Some(parent.into()),
);
let continued = sentry_core::start_transaction(ctx);
let continued_context = continued.get_trace_context();
assert_eq!(continued_context.trace_id, parent_context.trace_id);
assert_eq!(
continued_context.parent_span_id,
Some(parent_context.span_id)
);
});
}
#[test]
fn start_transaction_continues_when_dsn_org_id_matches_incoming() {
TraceContinuationScenario::run_with_options(
Some("42"),
ClientOptions::new()
.dsn("https://public@o42.ingest.us.sentry.io/1")
.strict_trace_continuation(true)
.traces_sample_rate(0.0),
)
.assert_continued();
}
#[test]
fn start_transaction_rejects_when_dsn_org_id_mismatches_incoming() {
TraceContinuationScenario::run_with_options(
Some("43"),
ClientOptions::new()
.dsn("https://public@o42.ingest.us.sentry.io/1")
.strict_trace_continuation(true)
.traces_sample_rate(0.0),
)
.assert_rejected();
}
#[test]
fn start_transaction_prefers_explicit_org_id_over_dsn_org_id() {
TraceContinuationScenario::run_with_options(
Some("42"),
ClientOptions::new()
.dsn("https://public@o43.ingest.us.sentry.io/1")
.org_id("42".parse().unwrap())
.strict_trace_continuation(true)
.traces_sample_rate(0.0),
)
.assert_continued();
}