From 18103bcd0c81980eaa2bf647835ba0f10b6c4368 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 Nov 2024 12:23:35 +0100 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] . --- 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/26] 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/26] . --- 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/26] . --- 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] . --- 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/26] 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/26] 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/26] 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 c5596693eb30853c8923378e4b8e901d002769f2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 12 Nov 2024 14:17:45 +0100 Subject: [PATCH 17/26] Use attributes instead of custom_sampling_context in WSGI --- MIGRATION_GUIDE.md | 1 + sentry_sdk/integrations/wsgi.py | 27 ++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 0095cafab7..179c783176 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 `wsgi_environ` object anymore. Instead, the individual properties are accessible as `wsgi_environ.PATH_INFO`... If you need more data accessible in the `traces_sampler`, provide additional `attributes` to your `start_span`. # TODO ### Removed diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 3aebff17d5..a317a24389 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -120,7 +120,7 @@ def __call__(self, environ, start_response): name=DEFAULT_TRANSACTION_NAME, source=TRANSACTION_SOURCE_ROUTE, origin=self.span_origin, - custom_sampling_context={"wsgi_environ": environ}, + attributes=_prepopulate_attributes(environ), ) if should_trace else nullcontext() @@ -309,3 +309,28 @@ def event_processor(event, hint): return event return event_processor + + +def _prepopulate_attributes(wsgi_environ): + attributes = {} + + for attr in ( + "CONTENT_LENGTH", + "CONTENT_TYPE", + "PATH_INFO", + "QUERY_STRING", + "REQUEST_METHOD", + "SCRIPT_NAME", + "SERVER_NAME", + "SERVER_PORT", + "SERVER_PROTOCOL", + "multithread", + "multiprocess", + "run_once", + "url_scheme", + "version", + ): + if wsgi_environ.get(attr): + attributes[f"wsgi_environ.{attr}"] = wsgi_environ[attr] + + return attributes From ef68e44d461350f18afc3282c9a5a3617ed074b3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 13 Nov 2024 15:14:58 +0100 Subject: [PATCH 18/26] . --- sentry_sdk/integrations/wsgi.py | 43 +++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index a317a24389..8fb27960a4 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -311,26 +311,33 @@ def event_processor(event, hint): return event_processor +ENVIRON_TO_ATTRIBUTE = { + "PATH_INFO": "url.path", + "REQUEST_METHOD": "http.request.method", + "SERVER_NAME": "server.address", + "SERVER_PORT": "server.port", + "url_scheme": "url.scheme", +} + + def _prepopulate_attributes(wsgi_environ): attributes = {} - for attr in ( - "CONTENT_LENGTH", - "CONTENT_TYPE", - "PATH_INFO", - "QUERY_STRING", - "REQUEST_METHOD", - "SCRIPT_NAME", - "SERVER_NAME", - "SERVER_PORT", - "SERVER_PROTOCOL", - "multithread", - "multiprocess", - "run_once", - "url_scheme", - "version", - ): - if wsgi_environ.get(attr): - attributes[f"wsgi_environ.{attr}"] = wsgi_environ[attr] + for property, attr in ENVIRON_TO_ATTRIBUTE.items(): + if wsgi_environ.get(property) is not None: + attributes[attr] = wsgi_environ[property] + + if wsgi_environ.get("SERVER_PROTOCOL") is not None: + try: + proto, version = wsgi_environ["SERVER_PROTOCOL"].split("/") + attributes["network.protocol.name"] = proto + attributes["network.protocol.version"] = version + except Exception: + attributes["network.protocol.name"] = wsgi_environ["SERVER_PROTOCOL"] + + try: + attributes["url.full"] = "" + except Exception: + pass return attributes From be0854046e4fbbcc01c70d46ce3c0379f3ffe1a1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 14 Nov 2024 13:52:36 +0100 Subject: [PATCH 19/26] todo --- MIGRATION_GUIDE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 04bfa939ec..d24875c80b 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -20,7 +20,12 @@ 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 `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties are accessible as `wsgi_environ.PATH_INFO`... If you need more data accessible in the `traces_sampler`, provide additional `attributes` to your `start_span`. # TODO +- The `sampling_context` argument of `traces_sampler` doesn't contain the `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties on the environment are accessible, if available, as follows: + + | Env property | Sampling context key(s) | + | ------------ | ----------------------- | + # TODO + - The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows: | Scope property | Sampling context key(s) | From c3fe091482b8ad8b29a66efa678aaa9030790d89 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 14 Nov 2024 16:49:29 +0100 Subject: [PATCH 20/26] url.full --- sentry_sdk/integrations/wsgi.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 8fb27960a4..bb8c1af85e 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -120,7 +120,9 @@ def __call__(self, environ, start_response): name=DEFAULT_TRANSACTION_NAME, source=TRANSACTION_SOURCE_ROUTE, origin=self.span_origin, - attributes=_prepopulate_attributes(environ), + attributes=_prepopulate_attributes( + environ, self.use_x_forwarded_for + ), ) if should_trace else nullcontext() @@ -313,14 +315,16 @@ def event_processor(event, hint): ENVIRON_TO_ATTRIBUTE = { "PATH_INFO": "url.path", + "QUERY_STRING": "url.query", "REQUEST_METHOD": "http.request.method", "SERVER_NAME": "server.address", "SERVER_PORT": "server.port", - "url_scheme": "url.scheme", + "wsgi.url_scheme": "url.scheme", } -def _prepopulate_attributes(wsgi_environ): +def _prepopulate_attributes(wsgi_environ, use_x_forwarded_for=False): + """Extract span attributes from the WSGI environment.""" attributes = {} for property, attr in ENVIRON_TO_ATTRIBUTE.items(): @@ -336,7 +340,9 @@ def _prepopulate_attributes(wsgi_environ): attributes["network.protocol.name"] = wsgi_environ["SERVER_PROTOCOL"] try: - attributes["url.full"] = "" + url = get_request_url(wsgi_environ, use_x_forwarded_for) + query = wsgi_environ.get("QUERY_STRING") + attributes["url.full"] = f"{url}?{query}" except Exception: pass From 31505a5678bf7e5d96d910b127e4084a0a6374f8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 14 Nov 2024 16:52:36 +0100 Subject: [PATCH 21/26] migration guide --- MIGRATION_GUIDE.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d24875c80b..bd3a2d73d3 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -22,9 +22,14 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - 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 `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties on the environment are accessible, if available, as follows: - | Env property | Sampling context key(s) | - | ------------ | ----------------------- | - # TODO + | Env property | Sampling context key | + | ------------ | ------------------------- | + | `PATH_INFO` | `url.path` | + | `QUERY_STRING` | `url.query` | + | `REQUEST_METHOD` | `http.request.method` | + | `SERVER_NAME` | `server.address` | + | `SERVER_PORT` | `server.port` | + | `wsgi.url_scheme` | `url.scheme` | - The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows: From 5dff8d85e4dae612332a254252869f80263044c6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 15 Nov 2024 09:13:50 +0100 Subject: [PATCH 22/26] formatting --- MIGRATION_GUIDE.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index bd3a2d73d3..feba132dbb 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -22,14 +22,14 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - 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 `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties on the environment are accessible, if available, as follows: - | Env property | Sampling context key | - | ------------ | ------------------------- | - | `PATH_INFO` | `url.path` | - | `QUERY_STRING` | `url.query` | - | `REQUEST_METHOD` | `http.request.method` | - | `SERVER_NAME` | `server.address` | - | `SERVER_PORT` | `server.port` | - | `wsgi.url_scheme` | `url.scheme` | + | Env property | Sampling context key | + | ----------------- | --------------------- | + | `PATH_INFO` | `url.path` | + | `QUERY_STRING` | `url.query` | + | `REQUEST_METHOD` | `http.request.method` | + | `SERVER_NAME` | `server.address` | + | `SERVER_PORT` | `server.port` | + | `wsgi.url_scheme` | `url.scheme` | - The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows: From 471ad4036e69e5d47ebe9947687cf4de67cf787f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 15 Nov 2024 09:18:19 +0100 Subject: [PATCH 23/26] missing keys --- MIGRATION_GUIDE.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 10ac1dc220..48b2679d94 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -21,18 +21,20 @@ 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 `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties on the environment are accessible, if available, as follows: - - | Env property | Sampling context key | - | ----------------- | --------------------- | - | `PATH_INFO` | `url.path` | - | `QUERY_STRING` | `url.query` | - | `REQUEST_METHOD` | `http.request.method` | - | `SERVER_NAME` | `server.address` | - | `SERVER_PORT` | `server.port` | - | `wsgi.url_scheme` | `url.scheme` | - -- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows: +- The `sampling_context` argument of `traces_sampler` doesn't contain the `wsgi_environ` object anymore for WSGI frameworks. Instead, the individual properties of the environment are accessible, if available, as follows: + + | Env property | Sampling context key(s) | + | ----------------- | ------------------------------------------------- | + | `PATH_INFO` | `url.path` | + | `QUERY_STRING` | `url.query` | + | `REQUEST_METHOD` | `http.request.method` | + | `SERVER_NAME` | `server.address` | + | `SERVER_PORT` | `server.port` | + | `SERVER_PROTOCOL` | `server.protocol.name`, `server.protocol.version` | + | `wsgi.url_scheme` | `url.scheme` | + | full URL | `url.full` | + +- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties of the scope, if available, are accessible as follows: | Scope property | Sampling context key(s) | | -------------- | ------------------------------- | From 1a38de3b1cf9711b448d61275e70df63aa7a7e83 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 15 Nov 2024 09:19:54 +0100 Subject: [PATCH 24/26] also add query for asgi --- MIGRATION_GUIDE.md | 1 + sentry_sdk/integrations/asgi.py | 1 + tests/integrations/asgi/test_asgi.py | 1 + 3 files changed, 3 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 48b2679d94..da84dc1758 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -41,6 +41,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh | `type` | `network.protocol.name` | | `scheme` | `url.scheme` | | `path` | `url.path` | + | `query` | `url.query` | | `http_version` | `network.protocol.version` | | `method` | `http.request.method` | | `server` | `server.address`, `server.port` | diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index b2ecfe23b7..80c24b8cb6 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -356,6 +356,7 @@ def _prepopulate_attributes(scope): full_url = _get_url(scope) query = _get_query(scope) if query: + attributes["url.query"] = query full_url = f"{full_url}?{query}" attributes["url.full"] = full_url diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 74f6d8cc49..adfd798c72 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -728,6 +728,7 @@ async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app): def dummy_traces_sampler(sampling_context): assert sampling_context["url.path"] == "/test" assert sampling_context["url.scheme"] == "http" + assert sampling_context["url.query"] == "hello=there" assert sampling_context["url.full"] == "/test?hello=there" assert sampling_context["http.request.method"] == "GET" assert sampling_context["network.protocol.version"] == "1.1" From 48ddc4f460ac646074bc0f5e54382954c900e73e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 15 Nov 2024 09:28:38 +0100 Subject: [PATCH 25/26] fix test --- tests/integrations/wsgi/test_wsgi.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 656fc1757f..0652a775d7 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -334,25 +334,21 @@ def app(environ, start_response): start_response("200 OK", []) return ["Go get the ball! Good dog!"] - traces_sampler = mock.Mock(return_value=True) + def traces_sampler(sampling_context): + assert sampling_context["http.request.method"] == "GET" + assert sampling_context["url.path"] == "/dogs/are/great/" + assert sampling_context["url.query"] == "cats=too" + assert sampling_context["url.scheme"] == "http" + assert ( + sampling_context["url.full"] == "http://localhost/dogs/are/great/?cats=too" + ) + return True + sentry_init(send_default_pii=True, traces_sampler=traces_sampler) app = SentryWsgiMiddleware(app) client = Client(app) - client.get("/dogs/are/great/") - - traces_sampler.assert_any_call( - DictionaryContaining( - { - "wsgi_environ": DictionaryContaining( - { - "PATH_INFO": "/dogs/are/great/", - "REQUEST_METHOD": "GET", - }, - ), - } - ) - ) + client.get("/dogs/are/great/?cats=too") def test_session_mode_defaults_to_request_mode_in_wsgi_handler( From affbe59de9c4b5ea90ab94ffd47c5af94362003d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 15 Nov 2024 09:30:03 +0100 Subject: [PATCH 26/26] reorg --- sentry_sdk/integrations/wsgi.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index bb8c1af85e..70324a3641 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -48,6 +48,15 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore DEFAULT_TRANSACTION_NAME = "generic WSGI request" +ENVIRON_TO_ATTRIBUTE = { + "PATH_INFO": "url.path", + "QUERY_STRING": "url.query", + "REQUEST_METHOD": "http.request.method", + "SERVER_NAME": "server.address", + "SERVER_PORT": "server.port", + "wsgi.url_scheme": "url.scheme", +} + def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): # type: (str, str, str) -> str @@ -313,16 +322,6 @@ def event_processor(event, hint): return event_processor -ENVIRON_TO_ATTRIBUTE = { - "PATH_INFO": "url.path", - "QUERY_STRING": "url.query", - "REQUEST_METHOD": "http.request.method", - "SERVER_NAME": "server.address", - "SERVER_PORT": "server.port", - "wsgi.url_scheme": "url.scheme", -} - - def _prepopulate_attributes(wsgi_environ, use_x_forwarded_for=False): """Extract span attributes from the WSGI environment.""" attributes = {}