From 18103bcd0c81980eaa2bf647835ba0f10b6c4368 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:23:35 +0100 Subject: [PATCH 01/21] Remove custom_sampling_context --- MIGRATION_GUIDE.md | 2 ++ sentry_sdk/api.py | 15 +++-------- .../integrations/opentelemetry/consts.py | 2 ++ .../integrations/opentelemetry/sampler.py | 24 ++++++++---------- .../integrations/opentelemetry/scope.py | 4 ++- sentry_sdk/tracing.py | 25 ++++++++++++++----- 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 495bfff75e..620cd7a3c5 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -19,11 +19,13 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Redis integration: In Redis pipeline spans there is no `span["data"]["redis.commands"]` that contains a dict `{"count": 3, "first_ten": ["cmd1", "cmd2", ...]}` but instead `span["data"]["redis.commands.count"]` (containing `3`) and `span["data"]["redis.commands.first_ten"]` (containing `["cmd1", "cmd2", ...]`). - clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`). - `sentry_sdk.init` now returns `None` instead of a context manager. +- The sampling context accessible in `traces_sampler` has changed. It now contains all span attributes. The original `sampling_context["transaction_context"]["name"]` and `sampling_context["transaction_context"]["op"]` are now accessible as `sampling_context["sentry_name"]` and `sampling_context["sentry_op"]`, respectively. ### Removed - Spans no longer have a `description`. Use `name` instead. - Dropped support for Python 3.6. +- The `custom_sampling_context` parameter of `start_transaction` has been removed. - The PyMongo integration no longer sets tags. The data is still accessible via span attributes. - The PyMongo integration doesn't set `operation_ids` anymore. The individual IDs (`operation_id`, `request_id`, `session_id`) are now accessible as separate span attributes. - `sentry_sdk.metrics` and associated metrics APIs have been removed as Sentry no longer accepts metrics data in this form. See https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 7d06abf660..a44d3f440e 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -40,7 +40,6 @@ ExcInfo, MeasurementUnit, LogLevelStr, - SamplingContext, ) from sentry_sdk.tracing import Span, TransactionKwargs @@ -239,12 +238,8 @@ def flush( return get_client().flush(timeout=timeout, callback=callback) -def start_span( - *, - custom_sampling_context=None, - **kwargs, # type: Any -): - # type: (...) -> POTelSpan +def start_span(**kwargs): + # type: (type.Any) -> POTelSpan """ Start and return a span. @@ -257,13 +252,11 @@ def start_span( of the `with` block. If not using context managers, call the `finish()` method. """ - # TODO: Consider adding type hints to the method signature. - return get_current_scope().start_span(custom_sampling_context, **kwargs) + return get_current_scope().start_span(**kwargs) def start_transaction( transaction=None, # type: Optional[Transaction] - custom_sampling_context=None, # type: Optional[SamplingContext] **kwargs, # type: Unpack[TransactionKwargs] ): # type: (...) -> POTelSpan @@ -295,14 +288,12 @@ def start_transaction( :param transaction: The transaction to start. If omitted, we create and start a new transaction. - :param custom_sampling_context: The transaction's custom sampling context. :param kwargs: Optional keyword arguments to be passed to the Transaction constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ return start_span( span=transaction, - custom_sampling_context=custom_sampling_context, **kwargs, ) diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 6d7c91f3f1..9cf19eb01d 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -30,3 +30,5 @@ class SentrySpanAttribute: NAME = "sentry.name" SOURCE = "sentry.source" CONTEXT = "sentry.context" + CUSTOM_SAMPLED = "sentry.custom_sampled" + PARENT_SAMPLED = "sentry.parent_sampled" diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index ed8ca36ebd..4420c8b428 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -2,7 +2,6 @@ from typing import cast from opentelemetry import trace - from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult, Decision from opentelemetry.trace.span import TraceState @@ -12,6 +11,7 @@ from sentry_sdk.integrations.opentelemetry.consts import ( TRACESTATE_SAMPLED_KEY, TRACESTATE_SAMPLE_RATE_KEY, + SentrySpanAttribute, ) from typing import TYPE_CHECKING @@ -118,25 +118,21 @@ def should_sample( if not has_tracing_enabled(client.options): return dropped_result(parent_span_context, attributes) - sample_rate = None + # Explicit sampled value provided at start_span + if attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED) is not None: + sample_rate = float(attributes[SentrySpanAttribute.CUSTOM_SAMPLED]) + return sampled_result(parent_span_context, attributes, sample_rate) - # Check if sampled=True was passed to start_transaction - # TODO-anton: Do we want to keep the start_transaction(sampled=True) thing? + sample_rate = None # 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")) if has_traces_sampler: - # TODO-anton: Make proper sampling_context - # TODO-neel-potel: Make proper sampling_context - sampling_context = { - "transaction_context": { - "name": name, - }, - "parent_sampled": get_parent_sampled(parent_span_context, trace_id), - } - - sample_rate = client.options["traces_sampler"](sampling_context) + attributes[SentrySpanAttribute.PARENT_SAMPLED] = get_parent_sampled( + parent_span_context, trace_id + ) + sample_rate = client.options["traces_sampler"](attributes) else: # Check if there is a parent with a sampling decision diff --git a/sentry_sdk/integrations/opentelemetry/scope.py b/sentry_sdk/integrations/opentelemetry/scope.py index 0c0087dae1..4274d70296 100644 --- a/sentry_sdk/integrations/opentelemetry/scope.py +++ b/sentry_sdk/integrations/opentelemetry/scope.py @@ -123,7 +123,9 @@ def start_transaction(self, custom_sampling_context=None, **kwargs): def start_span(self, custom_sampling_context=None, **kwargs): # type: (Optional[SamplingContext], Any) -> POTelSpan - return POTelSpan(**kwargs, scope=self) + return POTelSpan( + **kwargs, custom_sampling_context=custom_sampling_context, scope=self + ) _INITIAL_CURRENT_SCOPE = PotelScope(ty=ScopeType.CURRENT) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c2bd5734c5..868a47144f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -41,6 +41,8 @@ from typing_extensions import TypedDict, Unpack + from opentelemetry.utils import types as OTelSpanAttributes + P = ParamSpec("P") R = TypeVar("R") @@ -683,9 +685,9 @@ def get_trace_context(self): rv["status"] = self.status if self.containing_transaction: - rv[ - "dynamic_sampling_context" - ] = self.containing_transaction.get_baggage().dynamic_sampling_context() + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) data = {} @@ -1200,10 +1202,12 @@ 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] source=TRANSACTION_SOURCE_CUSTOM, # type: str + attributes=None, # type: OTelSpanAttributes otel_span=None, # type: Optional[OtelSpan] **_, # type: dict[str, object] ): @@ -1218,6 +1222,7 @@ def __init__( if otel_span is not None: self._otel_span = otel_span else: + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute from sentry_sdk.integrations.opentelemetry.utils import ( convert_to_otel_timestamp, ) @@ -1227,12 +1232,20 @@ def __init__( start_timestamp = convert_to_otel_timestamp(start_timestamp) span_name = name or description or op or "" - self._otel_span = tracer.start_span(span_name, start_time=start_timestamp) + + # Prepopulate some attrs to be accessible in traces_sampler + attributes = attributes or {} + attributes[SentrySpanAttribute.NAME] = span_name + attributes[SentrySpanAttribute.OP] = op + if sampled is not None: + attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled + + self._otel_span = tracer.start_span( + span_name, start_time=start_timestamp, attributes=attributes + ) self.origin = origin or DEFAULT_SPAN_ORIGIN - self.op = op self.description = description - self.name = span_name self.source = source if status is not None: From 68402fff97e3f7b77aa07c58b45ae784200df912 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:24:18 +0100 Subject: [PATCH 02/21] confusing wording --- MIGRATION_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 620cd7a3c5..b7c9db0241 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -19,7 +19,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Redis integration: In Redis pipeline spans there is no `span["data"]["redis.commands"]` that contains a dict `{"count": 3, "first_ten": ["cmd1", "cmd2", ...]}` but instead `span["data"]["redis.commands.count"]` (containing `3`) and `span["data"]["redis.commands.first_ten"]` (containing `["cmd1", "cmd2", ...]`). - clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`). - `sentry_sdk.init` now returns `None` instead of a context manager. -- The sampling context accessible in `traces_sampler` has changed. It now contains all span attributes. The original `sampling_context["transaction_context"]["name"]` and `sampling_context["transaction_context"]["op"]` are now accessible as `sampling_context["sentry_name"]` and `sampling_context["sentry_op"]`, respectively. +- The sampling context accessible in `traces_sampler` has changed. The original `sampling_context["transaction_context"]["name"]` and `sampling_context["transaction_context"]["op"]` are now accessible as `sampling_context["sentry_name"]` and `sampling_context["sentry_op"]`, respectively. ### Removed From fa5d0ba76c57fde8d1498d2b2dc1ae1c674867a8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:38:47 +0100 Subject: [PATCH 03/21] keep old format (why not) --- MIGRATION_GUIDE.md | 1 - sentry_sdk/integrations/opentelemetry/consts.py | 2 -- sentry_sdk/integrations/opentelemetry/sampler.py | 12 ++++++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index b7c9db0241..1c6daa0e29 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -19,7 +19,6 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Redis integration: In Redis pipeline spans there is no `span["data"]["redis.commands"]` that contains a dict `{"count": 3, "first_ten": ["cmd1", "cmd2", ...]}` but instead `span["data"]["redis.commands.count"]` (containing `3`) and `span["data"]["redis.commands.first_ten"]` (containing `["cmd1", "cmd2", ...]`). - clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`). - `sentry_sdk.init` now returns `None` instead of a context manager. -- The sampling context accessible in `traces_sampler` has changed. The original `sampling_context["transaction_context"]["name"]` and `sampling_context["transaction_context"]["op"]` are now accessible as `sampling_context["sentry_name"]` and `sampling_context["sentry_op"]`, respectively. ### Removed diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 9cf19eb01d..6d7c91f3f1 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -30,5 +30,3 @@ class SentrySpanAttribute: NAME = "sentry.name" SOURCE = "sentry.source" CONTEXT = "sentry.context" - CUSTOM_SAMPLED = "sentry.custom_sampled" - PARENT_SAMPLED = "sentry.parent_sampled" diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index 4420c8b428..d9bd777ef7 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -129,10 +129,14 @@ def should_sample( # Traces_sampler is responsible to check parent sampled to have full transactions. has_traces_sampler = callable(client.options.get("traces_sampler")) if has_traces_sampler: - attributes[SentrySpanAttribute.PARENT_SAMPLED] = get_parent_sampled( - parent_span_context, trace_id - ) - sample_rate = client.options["traces_sampler"](attributes) + sampling_context = { + "transaction_context": { + "name": name, + "op": attributes.get("op"), + }, + "parent_sampled": get_parent_sampled(parent_span_context, trace_id), + } + sample_rate = client.options["traces_sampler"](sampling_context) else: # Check if there is a parent with a sampling decision From 5df9ccc6336a513e11cdeb14c6339297c116eb0f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:40:36 +0100 Subject: [PATCH 04/21] . --- sentry_sdk/integrations/opentelemetry/consts.py | 1 + sentry_sdk/integrations/opentelemetry/sampler.py | 2 +- sentry_sdk/tracing.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 6d7c91f3f1..197e85cc8b 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -30,3 +30,4 @@ class SentrySpanAttribute: NAME = "sentry.name" SOURCE = "sentry.source" CONTEXT = "sentry.context" + CUSTOM_SAMPLED = "sentry.custom_sampled" diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index d9bd777ef7..2308ffafc1 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -132,7 +132,7 @@ def should_sample( sampling_context = { "transaction_context": { "name": name, - "op": attributes.get("op"), + "op": attributes.get(SentrySpanAttribute.OP), }, "parent_sampled": get_parent_sampled(parent_span_context, trace_id), } diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 868a47144f..94715591b6 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1235,7 +1235,6 @@ def __init__( # Prepopulate some attrs to be accessible in traces_sampler attributes = attributes or {} - attributes[SentrySpanAttribute.NAME] = span_name attributes[SentrySpanAttribute.OP] = op if sampled is not None: attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled @@ -1244,6 +1243,7 @@ def __init__( span_name, start_time=start_timestamp, attributes=attributes ) + self.name = name self.origin = origin or DEFAULT_SPAN_ORIGIN self.description = description self.source = source From 472ab5c23690fc7ae6cbd3c658654b36c8b31e1e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:41:37 +0100 Subject: [PATCH 05/21] add attrs --- sentry_sdk/integrations/opentelemetry/sampler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index 2308ffafc1..055abaef4d 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -136,6 +136,7 @@ def should_sample( }, "parent_sampled": get_parent_sampled(parent_span_context, trace_id), } + sampling_context.update(attributes) sample_rate = client.options["traces_sampler"](sampling_context) else: From f893f12a2deee3f5d6af89a767b92bb1d5a767e1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:42:31 +0100 Subject: [PATCH 06/21] . --- 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 94715591b6..1e712af161 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1233,7 +1233,7 @@ def __init__( span_name = name or description or op or "" - # Prepopulate some attrs to be accessible in traces_sampler + # Prepopulate some attrs so that they're accessible in traces_sampler attributes = attributes or {} attributes[SentrySpanAttribute.OP] = op if sampled is not None: From fff9f163cb4d94e9930d50530d2f5752f0aaae97 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:48:16 +0100 Subject: [PATCH 07/21] . --- sentry_sdk/integrations/opentelemetry/scope.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/opentelemetry/scope.py b/sentry_sdk/integrations/opentelemetry/scope.py index 4274d70296..82828d61de 100644 --- a/sentry_sdk/integrations/opentelemetry/scope.py +++ b/sentry_sdk/integrations/opentelemetry/scope.py @@ -24,7 +24,6 @@ from typing import Tuple, Optional, Generator, Dict, Any from typing_extensions import Unpack - from sentry_sdk._types import SamplingContext from sentry_sdk.tracing import TransactionKwargs @@ -112,20 +111,18 @@ def _incoming_otel_span_context(self): return span_context - def start_transaction(self, custom_sampling_context=None, **kwargs): - # type: (Optional[SamplingContext], Unpack[TransactionKwargs]) -> POTelSpan + def start_transaction(self, **kwargs): + # type: (Unpack[TransactionKwargs]) -> POTelSpan """ .. deprecated:: 3.0.0 This function is deprecated and will be removed in a future release. Use :py:meth:`sentry_sdk.start_span` instead. """ - return self.start_span(custom_sampling_context=custom_sampling_context) + return self.start_span(**kwargs) - def start_span(self, custom_sampling_context=None, **kwargs): - # type: (Optional[SamplingContext], Any) -> POTelSpan - return POTelSpan( - **kwargs, custom_sampling_context=custom_sampling_context, scope=self - ) + def start_span(self, **kwargs): + # type: (Any) -> POTelSpan + return POTelSpan(**kwargs, scope=self) _INITIAL_CURRENT_SCOPE = PotelScope(ty=ScopeType.CURRENT) From ce815e8ccee4d5940258b754bc13e16b432adf2f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:51:06 +0100 Subject: [PATCH 08/21] more removals --- sentry_sdk/scope.py | 8 +------- tests/tracing/test_sampling.py | 13 ------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 983b38bf2c..2a6700b178 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -946,9 +946,7 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): while len(self._breadcrumbs) > max_breadcrumbs: self._breadcrumbs.popleft() - def start_transaction( - self, transaction=None, custom_sampling_context=None, **kwargs - ): + def start_transaction(self, transaction=None, **kwargs): # type: (Optional[Transaction], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan] """ Start and return a transaction. @@ -974,7 +972,6 @@ def start_transaction( :param transaction: The transaction to start. If omitted, we create and start a new transaction. - :param custom_sampling_context: The transaction's custom sampling context. :param kwargs: Optional keyword arguments to be passed to the Transaction constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. @@ -985,8 +982,6 @@ def start_transaction( try_autostart_continuous_profiler() - custom_sampling_context = custom_sampling_context or {} - # if we haven't been given a transaction, make one transaction = Transaction(**kwargs) @@ -996,7 +991,6 @@ def start_transaction( "transaction_context": transaction.to_json(), "parent_sampled": transaction.parent_sampled, } - sampling_context.update(custom_sampling_context) transaction._set_initial_sampling_decision(sampling_context=sampling_context) if transaction.sampled: diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index 2e6ed0dab3..23cadf254c 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -211,19 +211,6 @@ def test_passes_parent_sampling_decision_in_sampling_context( assert sampling_context["parent_sampled"]._mock_wraps is parent_sampling_decision -def test_passes_custom_samling_context_from_start_transaction_to_traces_sampler( - sentry_init, DictionaryContaining # noqa: N803 -): - traces_sampler = mock.Mock() - sentry_init(traces_sampler=traces_sampler) - - start_transaction(custom_sampling_context={"dogs": "yes", "cats": "maybe"}) - - traces_sampler.assert_any_call( - DictionaryContaining({"dogs": "yes", "cats": "maybe"}) - ) - - def test_sample_rate_affects_errors(sentry_init, capture_events): sentry_init(sample_rate=0) events = capture_events() From 747ecc6ca113fbf2d3b5488732c649c7c1dcdccc Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 09:45:19 +0100 Subject: [PATCH 09/21] more readme --- MIGRATION_GUIDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 1c6daa0e29..64a21f17b6 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -19,12 +19,13 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Redis integration: In Redis pipeline spans there is no `span["data"]["redis.commands"]` that contains a dict `{"count": 3, "first_ten": ["cmd1", "cmd2", ...]}` but instead `span["data"]["redis.commands.count"]` (containing `3`) and `span["data"]["redis.commands.first_ten"]` (containing `["cmd1", "cmd2", ...]`). - clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`). - `sentry_sdk.init` now returns `None` instead of a context manager. +- The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start. ### Removed - Spans no longer have a `description`. Use `name` instead. - Dropped support for Python 3.6. -- The `custom_sampling_context` parameter of `start_transaction` has been removed. +- The `custom_sampling_context` parameter of `start_transaction` has been removed. Use `attributes` instead to set key-value pairs of data that should be accessible in the traces sampler. - The PyMongo integration no longer sets tags. The data is still accessible via span attributes. - The PyMongo integration doesn't set `operation_ids` anymore. The individual IDs (`operation_id`, `request_id`, `session_id`) are now accessible as separate span attributes. - `sentry_sdk.metrics` and associated metrics APIs have been removed as Sentry no longer accepts metrics data in this form. See https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics From 7ae6745076486ce851ce3375b75aa49869f7993a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 09:49:19 +0100 Subject: [PATCH 10/21] more info --- MIGRATION_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 64a21f17b6..0095cafab7 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -25,7 +25,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Spans no longer have a `description`. Use `name` instead. - Dropped support for Python 3.6. -- The `custom_sampling_context` parameter of `start_transaction` has been removed. Use `attributes` instead to set key-value pairs of data that should be accessible in the traces sampler. +- The `custom_sampling_context` parameter of `start_transaction` has been removed. Use `attributes` instead to set key-value pairs of data that should be accessible in the traces sampler. Note that span attributes need to conform to the [OpenTelemetry specification](https://opentelemetry.io/docs/concepts/signals/traces/#attributes), meaning only certain types can be set as values. - The PyMongo integration no longer sets tags. The data is still accessible via span attributes. - The PyMongo integration doesn't set `operation_ids` anymore. The individual IDs (`operation_id`, `request_id`, `session_id`) are now accessible as separate span attributes. - `sentry_sdk.metrics` and associated metrics APIs have been removed as Sentry no longer accepts metrics data in this form. See https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics From 09d247ec3a1ffe1a96533efd1ba8014645513f5c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 09:50:34 +0100 Subject: [PATCH 11/21] comment --- sentry_sdk/integrations/opentelemetry/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 197e85cc8b..1585e8d893 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -30,4 +30,4 @@ class SentrySpanAttribute: NAME = "sentry.name" SOURCE = "sentry.source" CONTEXT = "sentry.context" - CUSTOM_SAMPLED = "sentry.custom_sampled" + CUSTOM_SAMPLED = "sentry.custom_sampled" # used for saving start_span(sampled=X) From 1aeeb68d7d3a00de82ea2dd60181eb8fd61d6ac2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 09:53:05 +0100 Subject: [PATCH 12/21] small change --- 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 348e0c20ba..70744d2d71 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1257,9 +1257,9 @@ def __init__( span_name, start_time=start_timestamp, attributes=attributes ) - self.name = name self.origin = origin or DEFAULT_SPAN_ORIGIN self.description = description + self.name = span_name self.source = source if status is not None: From 786bb9ea3ad3a8987734097c93e4854019bf09e7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 10:33:36 +0100 Subject: [PATCH 13/21] . --- tests/tracing/test_sampling.py | 95 +++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index 23cadf254c..60daa50222 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -5,7 +5,7 @@ import pytest import sentry_sdk -from sentry_sdk import start_span, start_transaction, capture_exception +from sentry_sdk import start_span, capture_exception from sentry_sdk.tracing import Transaction from sentry_sdk.utils import logger @@ -13,7 +13,7 @@ def test_sampling_decided_only_for_transactions(sentry_init, capture_events): sentry_init(traces_sample_rate=0.5) - with start_transaction(name="hi") as transaction: + with start_span(name="hi") as transaction: assert transaction.sampled is not None with start_span() as span: @@ -24,16 +24,14 @@ def test_sampling_decided_only_for_transactions(sentry_init, capture_events): @pytest.mark.parametrize("sampled", [True, False]) -def test_nested_transaction_sampling_override(sentry_init, sampled): +def test_nested_span_sampling_override(sentry_init, sampled): sentry_init(traces_sample_rate=1.0) - with start_transaction(name="outer", sampled=sampled) as outer_transaction: - assert outer_transaction.sampled is sampled - with start_transaction( - name="inner", sampled=(not sampled) - ) as inner_transaction: - assert inner_transaction.sampled is not sampled - assert outer_transaction.sampled is sampled + 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: + assert inner_span.sampled is not sampled + assert outer_span.sampled is sampled def test_no_double_sampling(sentry_init, capture_events): @@ -42,19 +40,19 @@ def test_no_double_sampling(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0, sample_rate=0.0) events = capture_events() - with start_transaction(name="/"): + with start_span(name="/"): pass assert len(events) == 1 @pytest.mark.parametrize("sampling_decision", [True, False]) -def test_get_transaction_and_span_from_scope_regardless_of_sampling_decision( +def test_get_span_from_scope_regardless_of_sampling_decision( sentry_init, sampling_decision ): sentry_init(traces_sample_rate=1.0) - with start_transaction(name="/", sampled=sampling_decision): + with start_span(name="/", sampled=sampling_decision): with start_span(op="child-span"): with start_span(op="child-child-span"): scope = sentry_sdk.get_current_scope() @@ -74,8 +72,8 @@ def test_uses_traces_sample_rate_correctly( sentry_init(traces_sample_rate=traces_sample_rate) with mock.patch.object(random, "random", return_value=0.5): - transaction = start_transaction(name="dogpark") - assert transaction.sampled is expected_decision + span = start_span(name="dogpark") + assert span.sampled is expected_decision @pytest.mark.parametrize( @@ -90,8 +88,8 @@ def test_uses_traces_sampler_return_value_correctly( sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) with mock.patch.object(random, "random", return_value=0.5): - transaction = start_transaction(name="dogpark") - assert transaction.sampled is expected_decision + span = start_span(name="dogpark") + assert span.sampled is expected_decision @pytest.mark.parametrize("traces_sampler_return_value", [True, False]) @@ -100,19 +98,19 @@ def test_tolerates_traces_sampler_returning_a_boolean( ): sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) - transaction = start_transaction(name="dogpark") - assert transaction.sampled is traces_sampler_return_value + span = start_span(name="dogpark") + assert span.sampled is traces_sampler_return_value @pytest.mark.parametrize("sampling_decision", [True, False]) -def test_only_captures_transaction_when_sampled_is_true( +def test_only_captures_span_when_sampled_is_true( sentry_init, sampling_decision, capture_events ): sentry_init(traces_sampler=mock.Mock(return_value=sampling_decision)) events = capture_events() - transaction = start_transaction(name="dogpark") - transaction.finish() + span = start_span(name="dogpark") + span.finish() assert len(events) == (1 if sampling_decision else 0) @@ -133,9 +131,9 @@ def test_prefers_traces_sampler_to_traces_sample_rate( traces_sampler=traces_sampler, ) - transaction = start_transaction(name="dogpark") + span = start_span(name="dogpark") assert traces_sampler.called is True - assert transaction.sampled is traces_sampler_return_value + assert span.sampled is traces_sampler_return_value @pytest.mark.parametrize("parent_sampling_decision", [True, False]) @@ -147,10 +145,8 @@ def test_ignores_inherited_sample_decision_when_traces_sampler_defined( traces_sampler = mock.Mock(return_value=not parent_sampling_decision) sentry_init(traces_sampler=traces_sampler) - transaction = start_transaction( - name="dogpark", parent_sampled=parent_sampling_decision - ) - assert transaction.sampled is not parent_sampling_decision + span = start_span(name="dogpark", parent_sampled=parent_sampling_decision) + assert span.sampled is not parent_sampling_decision @pytest.mark.parametrize("explicit_decision", [True, False]) @@ -162,8 +158,8 @@ def test_traces_sampler_doesnt_overwrite_explicitly_passed_sampling_decision( traces_sampler = mock.Mock(return_value=not explicit_decision) sentry_init(traces_sampler=traces_sampler) - transaction = start_transaction(name="dogpark", sampled=explicit_decision) - assert transaction.sampled is explicit_decision + span = start_span(name="dogpark", sampled=explicit_decision) + assert span.sampled is explicit_decision @pytest.mark.parametrize("parent_sampling_decision", [True, False]) @@ -177,10 +173,8 @@ def test_inherits_parent_sampling_decision_when_traces_sampler_undefined( mock_random_value = 0.25 if parent_sampling_decision is False else 0.75 with mock.patch.object(random, "random", return_value=mock_random_value): - transaction = start_transaction( - name="dogpark", parent_sampled=parent_sampling_decision - ) - assert transaction.sampled is parent_sampling_decision + span = start_span(name="dogpark", parent_sampled=parent_sampling_decision) + assert span.sampled is parent_sampling_decision @pytest.mark.parametrize("parent_sampling_decision", [True, False]) @@ -195,11 +189,13 @@ def test_passes_parent_sampling_decision_in_sampling_context( ) ) + # XXX + transaction = Transaction.continue_from_headers( headers={"sentry-trace": sentry_trace_header}, name="dogpark" ) spy = mock.Mock(wraps=transaction) - start_transaction(transaction=spy) + start_span(span=spy) # there's only one call (so index at 0) and kwargs are always last in a call # tuple (so index at -1) @@ -223,6 +219,19 @@ def test_sample_rate_affects_errors(sentry_init, capture_events): assert len(events) == 0 +def test_passes_custom_attributes_from_start_span_to_traces_sampler( + sentry_init, DictionaryContaining # noqa: N803 +): + traces_sampler = mock.Mock() + sentry_init(traces_sampler=traces_sampler) + + start_span(attributes={"dogs": "yes", "cats": "maybe"}) + + traces_sampler.assert_any_call( + DictionaryContaining({"dogs": "yes", "cats": "maybe"}) + ) + + @pytest.mark.parametrize( "traces_sampler_return_value", [ @@ -243,9 +252,9 @@ def test_warns_and_sets_sampled_to_false_on_invalid_traces_sampler_return_value( sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) with mock.patch.object(logger, "warning", mock.Mock()): - transaction = start_transaction(name="dogpark") + span = start_span(name="dogpark") logger.warning.assert_any_call(StringContaining("Given sample rate is invalid")) - assert transaction.sampled is False + assert span.sampled is False @pytest.mark.parametrize( @@ -270,9 +279,9 @@ def test_records_lost_event_only_if_traces_sample_rate_enabled( sentry_init(traces_sample_rate=traces_sample_rate) record_lost_event_calls = capture_record_lost_event_calls() - transaction = start_transaction(name="dogpark") - assert transaction.sampled is sampled_output - transaction.finish() + span = start_span(name="dogpark") + assert span.sampled is sampled_output + span.finish() # Use Counter because order of calls does not matter assert Counter(record_lost_event_calls) == Counter(expected_record_lost_event_calls) @@ -300,9 +309,9 @@ def test_records_lost_event_only_if_traces_sampler_enabled( sentry_init(traces_sampler=traces_sampler) record_lost_event_calls = capture_record_lost_event_calls() - transaction = start_transaction(name="dogpark") - assert transaction.sampled is sampled_output - transaction.finish() + span = start_span(name="dogpark") + assert span.sampled is sampled_output + span.finish() # Use Counter because order of calls does not matter assert Counter(record_lost_event_calls) == Counter(expected_record_lost_event_calls) From 80427c6d4a304466c73b5897a1fcd0c74091dcd6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 11:09:55 +0100 Subject: [PATCH 14/21] dont do everything in this pr --- tests/tracing/test_sampling.py | 108 +++++++++++++++++---------------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index 60daa50222..70baacc951 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -5,7 +5,7 @@ import pytest import sentry_sdk -from sentry_sdk import start_span, capture_exception +from sentry_sdk import start_span, start_transaction, capture_exception from sentry_sdk.tracing import Transaction from sentry_sdk.utils import logger @@ -13,7 +13,7 @@ def test_sampling_decided_only_for_transactions(sentry_init, capture_events): sentry_init(traces_sample_rate=0.5) - with start_span(name="hi") as transaction: + with start_transaction(name="hi") as transaction: assert transaction.sampled is not None with start_span() as span: @@ -24,14 +24,16 @@ def test_sampling_decided_only_for_transactions(sentry_init, capture_events): @pytest.mark.parametrize("sampled", [True, False]) -def test_nested_span_sampling_override(sentry_init, sampled): +def test_nested_transaction_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: - assert inner_span.sampled is not sampled - assert outer_span.sampled is sampled + with start_transaction(name="outer", sampled=sampled) as outer_transaction: + assert outer_transaction.sampled is sampled + with start_transaction( + name="inner", sampled=(not sampled) + ) as inner_transaction: + assert inner_transaction.sampled is not sampled + assert outer_transaction.sampled is sampled def test_no_double_sampling(sentry_init, capture_events): @@ -40,19 +42,19 @@ def test_no_double_sampling(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0, sample_rate=0.0) events = capture_events() - with start_span(name="/"): + with start_transaction(name="/"): pass assert len(events) == 1 @pytest.mark.parametrize("sampling_decision", [True, False]) -def test_get_span_from_scope_regardless_of_sampling_decision( +def test_get_transaction_and_span_from_scope_regardless_of_sampling_decision( sentry_init, sampling_decision ): sentry_init(traces_sample_rate=1.0) - with start_span(name="/", sampled=sampling_decision): + with start_transaction(name="/", sampled=sampling_decision): with start_span(op="child-span"): with start_span(op="child-child-span"): scope = sentry_sdk.get_current_scope() @@ -72,8 +74,8 @@ def test_uses_traces_sample_rate_correctly( sentry_init(traces_sample_rate=traces_sample_rate) with mock.patch.object(random, "random", return_value=0.5): - span = start_span(name="dogpark") - assert span.sampled is expected_decision + transaction = start_transaction(name="dogpark") + assert transaction.sampled is expected_decision @pytest.mark.parametrize( @@ -88,8 +90,8 @@ def test_uses_traces_sampler_return_value_correctly( sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) with mock.patch.object(random, "random", return_value=0.5): - span = start_span(name="dogpark") - assert span.sampled is expected_decision + transaction = start_transaction(name="dogpark") + assert transaction.sampled is expected_decision @pytest.mark.parametrize("traces_sampler_return_value", [True, False]) @@ -98,19 +100,19 @@ def test_tolerates_traces_sampler_returning_a_boolean( ): sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) - span = start_span(name="dogpark") - assert span.sampled is traces_sampler_return_value + transaction = start_transaction(name="dogpark") + assert transaction.sampled is traces_sampler_return_value @pytest.mark.parametrize("sampling_decision", [True, False]) -def test_only_captures_span_when_sampled_is_true( +def test_only_captures_transaction_when_sampled_is_true( sentry_init, sampling_decision, capture_events ): sentry_init(traces_sampler=mock.Mock(return_value=sampling_decision)) events = capture_events() - span = start_span(name="dogpark") - span.finish() + transaction = start_transaction(name="dogpark") + transaction.finish() assert len(events) == (1 if sampling_decision else 0) @@ -131,9 +133,9 @@ def test_prefers_traces_sampler_to_traces_sample_rate( traces_sampler=traces_sampler, ) - span = start_span(name="dogpark") + transaction = start_transaction(name="dogpark") assert traces_sampler.called is True - assert span.sampled is traces_sampler_return_value + assert transaction.sampled is traces_sampler_return_value @pytest.mark.parametrize("parent_sampling_decision", [True, False]) @@ -145,8 +147,10 @@ def test_ignores_inherited_sample_decision_when_traces_sampler_defined( traces_sampler = mock.Mock(return_value=not parent_sampling_decision) sentry_init(traces_sampler=traces_sampler) - span = start_span(name="dogpark", parent_sampled=parent_sampling_decision) - assert span.sampled is not parent_sampling_decision + transaction = start_transaction( + name="dogpark", parent_sampled=parent_sampling_decision + ) + assert transaction.sampled is not parent_sampling_decision @pytest.mark.parametrize("explicit_decision", [True, False]) @@ -158,8 +162,8 @@ def test_traces_sampler_doesnt_overwrite_explicitly_passed_sampling_decision( traces_sampler = mock.Mock(return_value=not explicit_decision) sentry_init(traces_sampler=traces_sampler) - span = start_span(name="dogpark", sampled=explicit_decision) - assert span.sampled is explicit_decision + transaction = start_transaction(name="dogpark", sampled=explicit_decision) + assert transaction.sampled is explicit_decision @pytest.mark.parametrize("parent_sampling_decision", [True, False]) @@ -173,8 +177,10 @@ def test_inherits_parent_sampling_decision_when_traces_sampler_undefined( mock_random_value = 0.25 if parent_sampling_decision is False else 0.75 with mock.patch.object(random, "random", return_value=mock_random_value): - span = start_span(name="dogpark", parent_sampled=parent_sampling_decision) - assert span.sampled is parent_sampling_decision + transaction = start_transaction( + name="dogpark", parent_sampled=parent_sampling_decision + ) + assert transaction.sampled is parent_sampling_decision @pytest.mark.parametrize("parent_sampling_decision", [True, False]) @@ -189,13 +195,11 @@ def test_passes_parent_sampling_decision_in_sampling_context( ) ) - # XXX - transaction = Transaction.continue_from_headers( headers={"sentry-trace": sentry_trace_header}, name="dogpark" ) spy = mock.Mock(wraps=transaction) - start_span(span=spy) + start_transaction(transaction=spy) # there's only one call (so index at 0) and kwargs are always last in a call # tuple (so index at -1) @@ -207,6 +211,19 @@ def test_passes_parent_sampling_decision_in_sampling_context( assert sampling_context["parent_sampled"]._mock_wraps is parent_sampling_decision +def test_passes_attributes_from_start_span_to_traces_sampler( + sentry_init, DictionaryContaining # noqa: N803 +): + traces_sampler = mock.Mock() + sentry_init(traces_sampler=traces_sampler) + + start_transaction(attributes={"dogs": "yes", "cats": "maybe"}) + + traces_sampler.assert_any_call( + DictionaryContaining({"dogs": "yes", "cats": "maybe"}) + ) + + def test_sample_rate_affects_errors(sentry_init, capture_events): sentry_init(sample_rate=0) events = capture_events() @@ -219,19 +236,6 @@ def test_sample_rate_affects_errors(sentry_init, capture_events): assert len(events) == 0 -def test_passes_custom_attributes_from_start_span_to_traces_sampler( - sentry_init, DictionaryContaining # noqa: N803 -): - traces_sampler = mock.Mock() - sentry_init(traces_sampler=traces_sampler) - - start_span(attributes={"dogs": "yes", "cats": "maybe"}) - - traces_sampler.assert_any_call( - DictionaryContaining({"dogs": "yes", "cats": "maybe"}) - ) - - @pytest.mark.parametrize( "traces_sampler_return_value", [ @@ -252,9 +256,9 @@ def test_warns_and_sets_sampled_to_false_on_invalid_traces_sampler_return_value( sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) with mock.patch.object(logger, "warning", mock.Mock()): - span = start_span(name="dogpark") + transaction = start_transaction(name="dogpark") logger.warning.assert_any_call(StringContaining("Given sample rate is invalid")) - assert span.sampled is False + assert transaction.sampled is False @pytest.mark.parametrize( @@ -279,9 +283,9 @@ def test_records_lost_event_only_if_traces_sample_rate_enabled( sentry_init(traces_sample_rate=traces_sample_rate) record_lost_event_calls = capture_record_lost_event_calls() - span = start_span(name="dogpark") - assert span.sampled is sampled_output - span.finish() + transaction = start_transaction(name="dogpark") + assert transaction.sampled is sampled_output + transaction.finish() # Use Counter because order of calls does not matter assert Counter(record_lost_event_calls) == Counter(expected_record_lost_event_calls) @@ -309,9 +313,9 @@ def test_records_lost_event_only_if_traces_sampler_enabled( sentry_init(traces_sampler=traces_sampler) record_lost_event_calls = capture_record_lost_event_calls() - span = start_span(name="dogpark") - assert span.sampled is sampled_output - span.finish() + transaction = start_transaction(name="dogpark") + assert transaction.sampled is sampled_output + transaction.finish() # Use Counter because order of calls does not matter assert Counter(record_lost_event_calls) == Counter(expected_record_lost_event_calls) From 2acef08640fd3f15e0b429f423de3ea754cfa339 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 13:00:36 +0100 Subject: [PATCH 15/21] get rid of none attributes --- sentry_sdk/integrations/opentelemetry/sampler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index 055abaef4d..bb9d8cf377 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -114,6 +114,8 @@ def should_sample( parent_span_context = trace.get_current_span(parent_context).get_span_context() + attributes = attributes or {} + # No tracing enabled, thus no sampling if not has_tracing_enabled(client.options): return dropped_result(parent_span_context, attributes) From eb2df70b988c52fda9362181f0a965e1caffac4b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 13:05:41 +0100 Subject: [PATCH 16/21] dropped result too --- sentry_sdk/integrations/opentelemetry/sampler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index bb9d8cf377..79e2ec7d8f 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -123,7 +123,10 @@ def should_sample( # Explicit sampled value provided at start_span if attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED) is not None: sample_rate = float(attributes[SentrySpanAttribute.CUSTOM_SAMPLED]) - return sampled_result(parent_span_context, attributes, sample_rate) + if sample_rate > 0: + return sampled_result(parent_span_context, attributes, sample_rate) + else: + return dropped_result(parent_span_context, attributes) sample_rate = None From 2cee3737a4ef7108ae653bcc04c5d90ad38e3cc2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 13:32:39 +0100 Subject: [PATCH 17/21] Replace custom_sampling_context with attributes in ASGI --- MIGRATION_GUIDE.md | 1 + sentry_sdk/integrations/asgi.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 0095cafab7..8913550ae8 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -20,6 +20,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`). - `sentry_sdk.init` now returns `None` instead of a context manager. - The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start. +- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties, if available, are accessible as `asgi_scope.endpoint`, `asgi_scope.path`, `asgi_scope.root_path`, `asgi_scope.route`, `asgi_scope.scheme`, `asgi_scope.server` and `asgi_scope.type`. ### Removed diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index f67c47ef02..73801ed102 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -209,7 +209,7 @@ async def _run_app(self, scope, receive, send, asgi_version): name=transaction_name, source=transaction_source, origin=self.span_origin, - custom_sampling_context={"asgi_scope": scope}, + attributes=_prepopulate_attributes(scope), ) if should_trace else nullcontext() @@ -324,3 +324,16 @@ def _get_transaction_name_and_source(self, transaction_style, asgi_scope): return name, source return name, source + + +def _prepopulate_attributes(scope): + # type: (Any) -> dict[str, Any] + """Unpack asgi_scope into serializable attributes.""" + scope = scope or {} + + attributes = {} + for attr in ("endpoint", "path", "root_path", "route", "scheme", "server", "type"): + if scope.get(attr): + attributes[f"asgi_scope.{attr}"] = scope[attr] + + return attributes From f6fb9a91c704dae255fbb38fe61a0ec131362fe0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 14:37:13 +0100 Subject: [PATCH 18/21] add source too --- .../integrations/opentelemetry/sampler.py | 1 + sentry_sdk/tracing.py | 2 +- tests/integrations/asgi/test_asgi.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/opentelemetry/sampler.py b/sentry_sdk/integrations/opentelemetry/sampler.py index 79e2ec7d8f..3aaa563bd8 100644 --- a/sentry_sdk/integrations/opentelemetry/sampler.py +++ b/sentry_sdk/integrations/opentelemetry/sampler.py @@ -138,6 +138,7 @@ def should_sample( "transaction_context": { "name": name, "op": attributes.get(SentrySpanAttribute.OP), + "source": attributes.get(SentrySpanAttribute.SOURCE), }, "parent_sampled": get_parent_sampled(parent_span_context, trace_id), } diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 70744d2d71..0764e7c481 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1250,6 +1250,7 @@ def __init__( # Prepopulate some attrs so that they're accessible in traces_sampler attributes = attributes or {} attributes[SentrySpanAttribute.OP] = op + attributes[SentrySpanAttribute.SOURCE] = source if sampled is not None: attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled @@ -1260,7 +1261,6 @@ def __init__( self.origin = origin or DEFAULT_SPAN_ORIGIN self.description = description self.name = span_name - self.source = source if status is not None: self.set_status(status) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index e0a3900a38..fb97c385a0 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -721,3 +721,20 @@ async def test_custom_transaction_name( assert transaction_event["type"] == "transaction" assert transaction_event["transaction"] == "foobar" assert transaction_event["transaction_info"] == {"source": "custom"} + + +@pytest.mark.asyncio +async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app): + def dummy_traces_sampler(sampling_context): + assert sampling_context["asgi_scope.path"] == "/test" + assert sampling_context["asgi_scope.scheme"] == "http" + + sentry_init( + traces_sampler=dummy_traces_sampler, + traces_sample_rate=1.0, + ) + + app = SentryAsgiMiddleware(asgi3_app) + + async with TestClient(app) as client: + await client.get("/test") From 4002f38e1383409c553c9e041179708e3c0a8726 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 15:45:27 +0100 Subject: [PATCH 19/21] fix starlette tests --- tests/integrations/starlette/test_starlette.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 22ed10b7cb..c436b0b51a 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1066,7 +1066,11 @@ def dummy_traces_sampler(sampling_context): sentry_init( auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request. - integrations=[StarletteIntegration(transaction_style=transaction_style)], + integrations=[ + StarletteIntegration( + transaction_style=transaction_style, middleware_spans=False + ) + ], traces_sampler=dummy_traces_sampler, traces_sample_rate=1.0, ) From dbc8ba2eb79b404837fdec04683ee9e39a6a19c3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 15:46:50 +0100 Subject: [PATCH 20/21] fix fastapi --- tests/integrations/fastapi/test_fastapi.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 97aea06344..1dc180863a 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -458,7 +458,11 @@ def dummy_traces_sampler(sampling_context): sentry_init( auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request. - integrations=[StarletteIntegration(transaction_style=transaction_style)], + integrations=[ + StarletteIntegration( + transaction_style=transaction_style, middleware_spans=False + ) + ], traces_sampler=dummy_traces_sampler, traces_sample_rate=1.0, ) From 52f918a997a0e77452e3ee1c0e822f60cbb347c8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 13 Nov 2024 12:40:09 +0100 Subject: [PATCH 21/21] remove middleware spans toggle --- tests/integrations/fastapi/test_fastapi.py | 6 +----- tests/integrations/starlette/test_starlette.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 1dc180863a..97aea06344 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -458,11 +458,7 @@ def dummy_traces_sampler(sampling_context): sentry_init( auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request. - integrations=[ - StarletteIntegration( - transaction_style=transaction_style, middleware_spans=False - ) - ], + integrations=[StarletteIntegration(transaction_style=transaction_style)], traces_sampler=dummy_traces_sampler, traces_sample_rate=1.0, ) diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index c436b0b51a..22ed10b7cb 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1066,11 +1066,7 @@ def dummy_traces_sampler(sampling_context): sentry_init( auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request. - integrations=[ - StarletteIntegration( - transaction_style=transaction_style, middleware_spans=False - ) - ], + integrations=[StarletteIntegration(transaction_style=transaction_style)], traces_sampler=dummy_traces_sampler, traces_sample_rate=1.0, )