From 33d6f1bb0004087806b13ed3a3abf276e33692e8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Mar 2025 16:37:20 +0100 Subject: [PATCH 01/13] chore: Remove start_span(sampled=X) --- MIGRATION_GUIDE.md | 1 + .../integrations/opentelemetry/consts.py | 1 - .../integrations/opentelemetry/sampler.py | 20 -------------- sentry_sdk/tracing.py | 3 --- .../sqlalchemy/test_sqlalchemy.py | 16 ++++++------ tests/tracing/test_sampling.py | 26 ------------------- 6 files changed, 9 insertions(+), 58 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d57696d910..b218037acd 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -156,6 +156,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - `profiles_sample_rate` and `profiler_mode` were removed from options available via `_experiments`. Use the top-level `profiles_sample_rate` and `profiler_mode` options instead. - `Transport.capture_event` has been removed. Use `Transport.capture_envelope` instead. - Function transports are no longer supported. Subclass the `Transport` instead. +- `start_transaction` (`start_span`) no longer takes an explicit `sampled` argument. ### Deprecated diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 1585e8d893..6d7c91f3f1 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -30,4 +30,3 @@ class SentrySpanAttribute: NAME = "sentry.name" SOURCE = "sentry.source" CONTEXT = "sentry.context" - CUSTOM_SAMPLED = "sentry.custom_sampled" # used for saving start_span(sampled=X) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index a257f76f1e..3beb8a5c73 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -156,26 +156,6 @@ def should_sample( sample_rate = None - # Explicit sampled value provided at start_span - custom_sampled = cast( - "Optional[bool]", attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED) - ) - if custom_sampled is not None: - if is_root_span: - sample_rate = float(custom_sampled) - if sample_rate > 0: - return sampled_result( - parent_span_context, attributes, sample_rate=sample_rate - ) - else: - return dropped_result( - parent_span_context, attributes, sample_rate=sample_rate - ) - else: - logger.debug( - f"[Tracing] Ignoring sampled param for non-root span {name}" - ) - # Check if there is a traces_sampler # Traces_sampler is responsible to check parent sampled to have full transactions. has_traces_sampler = callable(client.options.get("traces_sampler")) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0e31ad4ff5..d49f2d2814 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -20,7 +20,6 @@ from sentry_sdk.utils import ( _serialize_span_attribute, get_current_thread_meta, - logger, should_be_treated_as_error, ) @@ -364,8 +363,6 @@ def __init__( attributes[SentrySpanAttribute.OP] = op if source is not None: attributes[SentrySpanAttribute.SOURCE] = source - if sampled is not None: - attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled parent_context = None if parent_span is not None: diff --git a/tests/integrations/sqlalchemy/test_sqlalchemy.py b/tests/integrations/sqlalchemy/test_sqlalchemy.py index 999b17a19f..2412d1495c 100644 --- a/tests/integrations/sqlalchemy/test_sqlalchemy.py +++ b/tests/integrations/sqlalchemy/test_sqlalchemy.py @@ -111,7 +111,7 @@ class Address(Base): Session = sessionmaker(bind=engine) # noqa: N806 session = Session() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): with session.begin_nested(): session.query(Person).first() @@ -185,7 +185,7 @@ class Address(Base): Session = sessionmaker(bind=engine) # noqa: N806 session = Session() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): with session.begin_nested(): session.query(Person).first() @@ -304,7 +304,7 @@ def test_query_source_disabled(sentry_init, capture_events): events = capture_events() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): Base = declarative_base() # noqa: N806 class Person(Base): @@ -356,7 +356,7 @@ def test_query_source_enabled(sentry_init, capture_events, enable_db_query_sourc events = capture_events() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): Base = declarative_base() # noqa: N806 class Person(Base): @@ -403,7 +403,7 @@ def test_query_source(sentry_init, capture_events): ) events = capture_events() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): Base = declarative_base() # noqa: N806 class Person(Base): @@ -473,7 +473,7 @@ def test_query_source_with_module_in_search_path(sentry_init, capture_events): query_first_model_from_session, ) - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): Base = declarative_base() # noqa: N806 class Person(Base): @@ -531,7 +531,7 @@ def test_no_query_source_if_duration_too_short(sentry_init, capture_events): ) events = capture_events() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): Base = declarative_base() # noqa: N806 class Person(Base): @@ -599,7 +599,7 @@ def test_query_source_if_duration_over_threshold(sentry_init, capture_events): ) events = capture_events() - with sentry_sdk.start_span(name="test_transaction", sampled=True): + with sentry_sdk.start_span(name="test_transaction"): Base = declarative_base() # noqa: N806 class Person(Base): diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index b418e5a572..06fb4c6240 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -23,19 +23,6 @@ def test_sampling_decided_only_for_root_spans(sentry_init): assert root_span2.sampled is not None -@pytest.mark.parametrize("sampled", [True, False]) -def test_nested_span_sampling_override(sentry_init, sampled): - sentry_init(traces_sample_rate=1.0) - - with start_span(name="outer", sampled=sampled) as outer_span: - assert outer_span.sampled is sampled - with start_span(name="inner", sampled=(not sampled)) as inner_span: - # won't work because the child span inherits the sampling decision - # from the parent - assert inner_span.sampled is sampled - assert outer_span.sampled is sampled - - def test_no_double_sampling(sentry_init, capture_events): # Transactions should not be subject to the global/error sample rate. # Only the traces_sample_rate should apply. @@ -146,19 +133,6 @@ def test_ignores_inherited_sample_decision_when_traces_sampler_defined( assert span.sampled is not parent_sampling_decision -@pytest.mark.parametrize("explicit_decision", [True, False]) -def test_traces_sampler_doesnt_overwrite_explicitly_passed_sampling_decision( - sentry_init, explicit_decision -): - # make traces_sampler pick the opposite of the explicit decision, to prove - # that the explicit decision takes precedence - traces_sampler = mock.Mock(return_value=not explicit_decision) - sentry_init(traces_sampler=traces_sampler) - - with start_span(name="dogpark", sampled=explicit_decision) as span: - assert span.sampled is explicit_decision - - @pytest.mark.parametrize("parent_sampling_decision", [True, False]) def test_inherits_parent_sampling_decision_when_traces_sampler_undefined( sentry_init, parent_sampling_decision From 2a853a49772613baa43f599a811b1af0150e6042 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Mar 2025 16:40:35 +0100 Subject: [PATCH 02/13] let it fail loudly rather than silently --- sentry_sdk/tracing.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d49f2d2814..a70c97e340 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -309,7 +309,6 @@ def __init__( op=None, # type: Optional[str] description=None, # type: Optional[str] status=None, # type: Optional[str] - sampled=None, # type: Optional[bool] start_timestamp=None, # type: Optional[Union[datetime, float]] origin=None, # type: Optional[str] name=None, # type: Optional[str] @@ -318,14 +317,9 @@ def __init__( only_if_parent=False, # type: bool parent_span=None, # type: Optional[Span] otel_span=None, # type: Optional[OtelSpan] - **_, # type: dict[str, object] ): # type: (...) -> None """ - For backwards compatibility with old the old Span interface, this class - accepts arbitrary keyword arguments, in addition to the ones explicitly - listed in the signature. These additional arguments are ignored. - If otel_span is passed explicitly, just acts as a proxy. If only_if_parent is True, just return an INVALID_SPAN From bad71c7267cfa95050ea37c574947db7eed07755 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Mar 2025 16:45:05 +0100 Subject: [PATCH 03/13] Deprecate more stuff --- MIGRATION_GUIDE.md | 3 +++ sentry_sdk/tracing.py | 48 +------------------------------------------ 2 files changed, 4 insertions(+), 47 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index b218037acd..2ebdc2ae1b 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -157,6 +157,9 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - `Transport.capture_event` has been removed. Use `Transport.capture_envelope` instead. - Function transports are no longer supported. Subclass the `Transport` instead. - `start_transaction` (`start_span`) no longer takes an explicit `sampled` argument. +- `start_transaction` (`start_span`) no longer takes an explicit `description` argument. Use `name` instead. +- `start_transaction` (`start_span`) no longer takes an explicit `trace_id` or `span_id` argument. Use `continue_trace` for propagation from headers or environment variables. + ### Deprecated diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index a70c97e340..e7984fd2a2 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -51,36 +51,12 @@ from sentry_sdk.tracing_utils import Baggage class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - op: str """ The span's operation. A list of recommended values is available here: https://develop.sentry.dev/sdk/performance/span-operations/ """ - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - status: str """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" @@ -114,12 +90,6 @@ class TransactionKwargs(SpanKwargs, total=False): Default "custom". """ - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - ProfileContext = TypedDict( "ProfileContext", { @@ -307,7 +277,6 @@ def __init__( self, *, op=None, # type: Optional[str] - description=None, # type: Optional[str] status=None, # type: Optional[str] start_timestamp=None, # type: Optional[Union[datetime, float]] origin=None, # type: Optional[str] @@ -349,7 +318,7 @@ def __init__( # OTel timestamps have nanosecond precision start_timestamp = convert_to_otel_timestamp(start_timestamp) - span_name = name or description or op or DEFAULT_SPAN_NAME + span_name = name or op or DEFAULT_SPAN_NAME # Prepopulate some attrs so that they're accessible in traces_sampler attributes = attributes or {} @@ -372,7 +341,6 @@ def __init__( ) self.origin = origin or DEFAULT_SPAN_ORIGIN - self.description = description self.name = span_name if status is not None: @@ -431,20 +399,6 @@ def __exit__(self, ty, value, tb): context.detach(self._ctx_token) del self._ctx_token - @property - def description(self): - # type: () -> Optional[str] - from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - - return self.get_attribute(SentrySpanAttribute.DESCRIPTION) - - @description.setter - def description(self, value): - # type: (Optional[str]) -> None - from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - - self.set_attribute(SentrySpanAttribute.DESCRIPTION, value) - @property def origin(self): # type: () -> Optional[str] From fb745309f111d49350bda39b45d5e27f76d1b683 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Mar 2025 16:54:37 +0100 Subject: [PATCH 04/13] . --- MIGRATION_GUIDE.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 2ebdc2ae1b..2dfee8f85c 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -156,9 +156,13 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - `profiles_sample_rate` and `profiler_mode` were removed from options available via `_experiments`. Use the top-level `profiles_sample_rate` and `profiler_mode` options instead. - `Transport.capture_event` has been removed. Use `Transport.capture_envelope` instead. - Function transports are no longer supported. Subclass the `Transport` instead. -- `start_transaction` (`start_span`) no longer takes an explicit `sampled` argument. -- `start_transaction` (`start_span`) no longer takes an explicit `description` argument. Use `name` instead. -- `start_transaction` (`start_span`) no longer takes an explicit `trace_id` or `span_id` argument. Use `continue_trace` for propagation from headers or environment variables. +- `start_transaction` (`start_span`) no longer takes the following arguments: + - `sampled`: use a `traces_sampler` to adjust the sampling rate + - `description`: use `name` instead + - `trace_id`, `baggage`: use `continue_trace` for propagation from headers or environment variables + - `same_process_as_parent` + - `span_id` + - `parent_span_id`: you can supply a `parent_span` instead ### Deprecated From ec1dd3ab8dde81423bd273ea4da536d9051e3062 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 4 Apr 2025 13:41:30 +0200 Subject: [PATCH 05/13] bring back description --- sentry_sdk/tracing.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index e7984fd2a2..d7761704c0 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -399,6 +399,20 @@ def __exit__(self, ty, value, tb): context.detach(self._ctx_token) del self._ctx_token + @property + def description(self): + # type: () -> Optional[str] + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + return self.get_attribute(SentrySpanAttribute.DESCRIPTION) + + @description.setter + def description(self, value): + # type: (Optional[str]) -> None + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + self.set_attribute(SentrySpanAttribute.DESCRIPTION, value) + @property def origin(self): # type: () -> Optional[str] From a4cf07eb331309a70b380719dfd3cbd2589f399c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 4 Apr 2025 13:43:17 +0200 Subject: [PATCH 06/13] desc --- sentry_sdk/tracing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d7761704c0..d42f14632a 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -277,6 +277,7 @@ def __init__( self, *, op=None, # type: Optional[str] + description=None, # type: Optional[str] status=None, # type: Optional[str] start_timestamp=None, # type: Optional[Union[datetime, float]] origin=None, # type: Optional[str] @@ -326,6 +327,8 @@ def __init__( attributes[SentrySpanAttribute.OP] = op if source is not None: attributes[SentrySpanAttribute.SOURCE] = source + if description is not None: + attributes[SentrySpanAttribute.DESCRIPTION] = description parent_context = None if parent_span is not None: From 4163da0939c416e7a3baa231e5f1fe7a6f36b188 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 4 Apr 2025 13:44:29 +0200 Subject: [PATCH 07/13] . --- sentry_sdk/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d42f14632a..0e72c0a9cb 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -319,7 +319,7 @@ def __init__( # OTel timestamps have nanosecond precision start_timestamp = convert_to_otel_timestamp(start_timestamp) - span_name = name or op or DEFAULT_SPAN_NAME + span_name = name or description or op or DEFAULT_SPAN_NAME # Prepopulate some attrs so that they're accessible in traces_sampler attributes = attributes or {} From fe1e82e2b3fecdc22d90c3133d4b1b95c13f00e0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 4 Apr 2025 13:45:59 +0200 Subject: [PATCH 08/13] . --- sentry_sdk/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0e72c0a9cb..8797b9cdce 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -585,7 +585,7 @@ def timestamp(self): def start_child(self, **kwargs): # type: (**Any) -> Span - return Span(sampled=self.sampled, parent_span=self, **kwargs) + return Span(parent_span=self, **kwargs) def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] From 7277307daeb7530757f7fa6848c74a7c7d4109c9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 4 Apr 2025 13:49:26 +0200 Subject: [PATCH 09/13] ... --- MIGRATION_GUIDE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 4fc875b67a..5ebb29107a 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -131,7 +131,6 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ### Removed -- Spans no longer have a `description`. Use `name` instead. - Dropped support for Python 3.6. - The `enable_tracing` `init` option has been removed. Configure `traces_sample_rate` directly. - The `propagate_traces` `init` option has been removed. Use `trace_propagation_targets` instead. @@ -159,7 +158,6 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Function transports are no longer supported. Subclass the `Transport` instead. - `start_transaction` (`start_span`) no longer takes the following arguments: - `sampled`: use a `traces_sampler` to adjust the sampling rate - - `description`: use `name` instead - `trace_id`, `baggage`: use `continue_trace` for propagation from headers or environment variables - `same_process_as_parent` - `span_id` From 675384041ef6af3b714fc6c3d0b8db89782d9a9a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 8 Apr 2025 15:36:41 +0200 Subject: [PATCH 10/13] readd desc to spankwargs --- sentry_sdk/tracing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 8797b9cdce..7485f986d5 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -57,6 +57,9 @@ class SpanKwargs(TypedDict, total=False): https://develop.sentry.dev/sdk/performance/span-operations/ """ + description: str + """A description of what operation is being performed within the span.""" + status: str """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" From 07c12d5715f0e5ed6efd942d09054648bbb0d971 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 9 Apr 2025 10:46:45 +0200 Subject: [PATCH 11/13] Revert "chore: Remove start_span(sampled=X)" This reverts commit 33d6f1bb0004087806b13ed3a3abf276e33692e8. --- MIGRATION_GUIDE.md | 1 - .../integrations/opentelemetry/consts.py | 1 + .../integrations/opentelemetry/sampler.py | 20 ++++++++++++++ sentry_sdk/tracing.py | 3 +++ .../sqlalchemy/test_sqlalchemy.py | 16 ++++++------ tests/tracing/test_sampling.py | 26 +++++++++++++++++++ 6 files changed, 58 insertions(+), 9 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 10602ff0af..68717c5c14 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -157,7 +157,6 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - `Transport.capture_event` has been removed. Use `Transport.capture_envelope` instead. - Function transports are no longer supported. Subclass the `Transport` instead. - `start_transaction` (`start_span`) no longer takes the following arguments: - - `sampled`: use a `traces_sampler` to adjust the sampling rate - `trace_id`, `baggage`: use `continue_trace` for propagation from headers or environment variables - `same_process_as_parent` - `span_id` diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 16d4eed11e..d4b2b47768 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -33,3 +33,4 @@ class SentrySpanAttribute: NAME = "sentry.name" SOURCE = "sentry.source" CONTEXT = "sentry.context" + CUSTOM_SAMPLED = "sentry.custom_sampled" # used for saving start_span(sampled=X) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index e743731ef8..a41f9756d3 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -211,6 +211,26 @@ def should_sample( # We are the head SDK and we need to generate a new sample_rand sample_rand = cast(Decimal, _generate_sample_rand(str(trace_id), (0, 1))) + # Explicit sampled value provided at start_span + custom_sampled = cast( + "Optional[bool]", attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED) + ) + if custom_sampled is not None: + if is_root_span: + sample_rate = float(custom_sampled) + if sample_rate > 0: + return sampled_result( + parent_span_context, attributes, sample_rate=sample_rate + ) + else: + return dropped_result( + parent_span_context, attributes, sample_rate=sample_rate + ) + else: + logger.debug( + f"[Tracing] Ignoring sampled param for non-root span {name}" + ) + # Check if there is a traces_sampler # Traces_sampler is responsible to check parent sampled to have full transactions. has_traces_sampler = callable(client.options.get("traces_sampler")) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 7485f986d5..6ee5f615b2 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -282,6 +282,7 @@ def __init__( op=None, # type: Optional[str] description=None, # type: Optional[str] status=None, # type: Optional[str] + sampled=None, # type: Optional[bool] start_timestamp=None, # type: Optional[Union[datetime, float]] origin=None, # type: Optional[str] name=None, # type: Optional[str] @@ -332,6 +333,8 @@ def __init__( attributes[SentrySpanAttribute.SOURCE] = source if description is not None: attributes[SentrySpanAttribute.DESCRIPTION] = description + if sampled is not None: + attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled parent_context = None if parent_span is not None: diff --git a/tests/integrations/sqlalchemy/test_sqlalchemy.py b/tests/integrations/sqlalchemy/test_sqlalchemy.py index 2412d1495c..999b17a19f 100644 --- a/tests/integrations/sqlalchemy/test_sqlalchemy.py +++ b/tests/integrations/sqlalchemy/test_sqlalchemy.py @@ -111,7 +111,7 @@ class Address(Base): Session = sessionmaker(bind=engine) # noqa: N806 session = Session() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): with session.begin_nested(): session.query(Person).first() @@ -185,7 +185,7 @@ class Address(Base): Session = sessionmaker(bind=engine) # noqa: N806 session = Session() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): with session.begin_nested(): session.query(Person).first() @@ -304,7 +304,7 @@ def test_query_source_disabled(sentry_init, capture_events): events = capture_events() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): Base = declarative_base() # noqa: N806 class Person(Base): @@ -356,7 +356,7 @@ def test_query_source_enabled(sentry_init, capture_events, enable_db_query_sourc events = capture_events() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): Base = declarative_base() # noqa: N806 class Person(Base): @@ -403,7 +403,7 @@ def test_query_source(sentry_init, capture_events): ) events = capture_events() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): Base = declarative_base() # noqa: N806 class Person(Base): @@ -473,7 +473,7 @@ def test_query_source_with_module_in_search_path(sentry_init, capture_events): query_first_model_from_session, ) - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): Base = declarative_base() # noqa: N806 class Person(Base): @@ -531,7 +531,7 @@ def test_no_query_source_if_duration_too_short(sentry_init, capture_events): ) events = capture_events() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): Base = declarative_base() # noqa: N806 class Person(Base): @@ -599,7 +599,7 @@ def test_query_source_if_duration_over_threshold(sentry_init, capture_events): ) events = capture_events() - with sentry_sdk.start_span(name="test_transaction"): + with sentry_sdk.start_span(name="test_transaction", sampled=True): Base = declarative_base() # noqa: N806 class Person(Base): diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index cec8a38a54..59780729b7 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -23,6 +23,19 @@ def test_sampling_decided_only_for_root_spans(sentry_init): assert root_span2.sampled is not None +@pytest.mark.parametrize("sampled", [True, False]) +def test_nested_span_sampling_override(sentry_init, sampled): + sentry_init(traces_sample_rate=1.0) + + with start_span(name="outer", sampled=sampled) as outer_span: + assert outer_span.sampled is sampled + with start_span(name="inner", sampled=(not sampled)) as inner_span: + # won't work because the child span inherits the sampling decision + # from the parent + assert inner_span.sampled is sampled + assert outer_span.sampled is sampled + + def test_no_double_sampling(sentry_init, capture_events): # Transactions should not be subject to the global/error sample rate. # Only the traces_sample_rate should apply. @@ -143,6 +156,19 @@ def test_ignores_inherited_sample_decision_when_traces_sampler_defined( assert span.sampled is not parent_sampling_decision +@pytest.mark.parametrize("explicit_decision", [True, False]) +def test_traces_sampler_doesnt_overwrite_explicitly_passed_sampling_decision( + sentry_init, explicit_decision +): + # make traces_sampler pick the opposite of the explicit decision, to prove + # that the explicit decision takes precedence + traces_sampler = mock.Mock(return_value=not explicit_decision) + sentry_init(traces_sampler=traces_sampler) + + with start_span(name="dogpark", sampled=explicit_decision) as span: + assert span.sampled is explicit_decision + + @pytest.mark.parametrize("parent_sampling_decision", [True, False]) def test_inherits_parent_sampling_decision_when_traces_sampler_undefined( sentry_init, parent_sampling_decision From 8b8ef5e91222a32458af847a33d2878c5cae76e9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 9 Apr 2025 10:50:07 +0200 Subject: [PATCH 12/13] oversight --- sentry_sdk/integrations/opentelemetry/sampler.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index a41f9756d3..83d647f1d8 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -220,11 +220,17 @@ def should_sample( sample_rate = float(custom_sampled) if sample_rate > 0: return sampled_result( - parent_span_context, attributes, sample_rate=sample_rate + parent_span_context, + attributes, + sample_rate=sample_rate, + sample_rand=sample_rand, ) else: return dropped_result( - parent_span_context, attributes, sample_rate=sample_rate + parent_span_context, + attributes, + sample_rate=sample_rate, + sample_rand=sample_rand, ) else: logger.debug( From 8aabeb7f43eddc39fd98075eacf31a7cd3b5a7c3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 9 Apr 2025 11:16:19 +0200 Subject: [PATCH 13/13] . --- sentry_sdk/tracing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 5ff6b39bb2..5a06d704ee 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -298,6 +298,7 @@ def __init__( ) self.origin = origin or DEFAULT_SPAN_ORIGIN + self.description = description self.name = span_name if status is not None: