From c6dfd3693b51d88f2ea82dd83ded5f2da5f2be75 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 11:38:38 +0200 Subject: [PATCH 01/16] Fix circular imports --- sentry_sdk/integrations/opentelemetry/__init__.py | 13 ++++++------- .../integrations/opentelemetry/span_processor.py | 3 ++- sentry_sdk/integrations/opentelemetry/utils.py | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/opentelemetry/__init__.py b/sentry_sdk/integrations/opentelemetry/__init__.py index 43587f2e01..e0020204d5 100644 --- a/sentry_sdk/integrations/opentelemetry/__init__.py +++ b/sentry_sdk/integrations/opentelemetry/__init__.py @@ -1,8 +1,7 @@ -# TODO-neel-potel fix circular imports -# from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401 -# SentrySpanProcessor, -# ) +from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401 + SentrySpanProcessor, +) -# from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401 -# SentryPropagator, -# ) +from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401 + SentryPropagator, +) diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py index 594ccbb71f..7671c798c8 100644 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -13,7 +13,6 @@ INVALID_SPAN_ID, INVALID_TRACE_ID, ) -from sentry_sdk import get_client, start_transaction from sentry_sdk.integrations.opentelemetry.consts import ( SENTRY_BAGGAGE_KEY, SENTRY_TRACE_KEY, @@ -106,6 +105,8 @@ def _prune_old_spans(self): def on_start(self, otel_span, parent_context=None): # type: (OTelSpan, Optional[context_api.Context]) -> None + from sentry_sdk import get_client, start_transaction + client = get_client() if not client.dsn: diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index cb04dd8e1a..df668799cf 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -8,7 +8,6 @@ from sentry_sdk.tracing import get_span_status_from_http_code from urllib3.util import parse_url as urlparse -from sentry_sdk import get_client from sentry_sdk.utils import Dsn from sentry_sdk._types import TYPE_CHECKING @@ -43,6 +42,8 @@ def is_sentry_span(span): Break infinite loop: HTTP requests to Sentry are caught by OTel and send again to Sentry. """ + from sentry_sdk import get_client + if not span.attributes: return False From 302806fd138f5ee2ca30c3017deb73b1a3817520 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 12:45:09 +0200 Subject: [PATCH 02/16] Fix origin --- .../opentelemetry/potel_span_processor.py | 8 ++--- .../opentelemetry/span_processor.py | 4 +-- .../integrations/opentelemetry/utils.py | 32 ++++++++++--------- .../integrations/opentelemetry/test_utils.py | 7 +++- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py index 9604676dce..cddaf24ab2 100644 --- a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -116,12 +116,12 @@ def _root_span_to_transaction_event(self, span): span_id = format_span_id(span.context.span_id) parent_span_id = format_span_id(span.parent.span_id) if span.parent else None - (op, description, status, _) = extract_span_data(span) + (op, description, status, _, origin) = extract_span_data(span) trace_context = { "trace_id": trace_id, "span_id": span_id, - "origin": SPAN_ORIGIN, + "origin": origin, "op": op, "status": status, } # type: dict[str, Any] @@ -160,17 +160,17 @@ def _span_to_json(self, span): span_id = format_span_id(span.context.span_id) parent_span_id = format_span_id(span.parent.span_id) if span.parent else None - (op, description, status, _) = extract_span_data(span) + (op, description, status, _, origin) = extract_span_data(span) span_json = { "trace_id": trace_id, "span_id": span_id, - "origin": SPAN_ORIGIN, "op": op, "description": description, "status": status, "start_timestamp": convert_otel_timestamp(span.start_time), "timestamp": convert_otel_timestamp(span.end_time), + "origin": origin or SPAN_ORIGIN, } # type: dict[str, Any] if parent_span_id: diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py index 7671c798c8..2140b0e70b 100644 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -259,7 +259,7 @@ def _update_span_with_otel_data(self, sentry_span, otel_span): for key, val in otel_span.attributes.items(): sentry_span.set_data(key, val) - (op, description, status, http_status) = extract_span_data(otel_span) + (op, description, status, http_status, _) = extract_span_data(otel_span) sentry_span.op = op sentry_span.description = description @@ -270,7 +270,7 @@ def _update_span_with_otel_data(self, sentry_span, otel_span): def _update_transaction_with_otel_data(self, sentry_span, otel_span): # type: (SentrySpan, OTelSpan) -> None - (op, _, status, http_status) = extract_span_data(otel_span) + (op, _, status, http_status, _) = extract_span_data(otel_span) sentry_span.op = op if http_status: diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index df668799cf..4d2e02ef9d 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -6,6 +6,7 @@ from opentelemetry.sdk.trace import ReadableSpan from sentry_sdk.consts import SPANSTATUS from sentry_sdk.tracing import get_span_status_from_http_code +from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute from urllib3.util import parse_url as urlparse from sentry_sdk.utils import Dsn @@ -77,13 +78,16 @@ def convert_otel_timestamp(time): def extract_span_data(span): - # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int]] + # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] op = span.name description = span.name status, http_status = extract_span_status(span) + origin = None if span.attributes is None: - return (op, description, status, http_status) + return (op, description, status, http_status, origin) + + origin = span.attributes.get(SentrySpanAttribute.ORIGIN) http_method = span.attributes.get(SpanAttributes.HTTP_METHOD) http_method = cast("Optional[str]", http_method) @@ -96,26 +100,21 @@ def extract_span_data(span): rpc_service = span.attributes.get(SpanAttributes.RPC_SERVICE) if rpc_service: - return ("rpc", description, status, http_status) + return ("rpc", description, status, http_status, origin) messaging_system = span.attributes.get(SpanAttributes.MESSAGING_SYSTEM) if messaging_system: - return ("message", description, status, http_status) + return ("message", description, status, http_status, origin) faas_trigger = span.attributes.get(SpanAttributes.FAAS_TRIGGER) if faas_trigger: - return ( - str(faas_trigger), - description, - status, - http_status, - ) + return (str(faas_trigger), description, status, http_status, origin) - return (op, description, status, http_status) + return (op, description, status, http_status, origin) def span_data_for_http_method(span): - # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int]] + # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] span_attributes = span.attributes or {} op = "http" @@ -151,11 +150,13 @@ def span_data_for_http_method(span): status, http_status = extract_span_status(span) - return (op, description, status, http_status) + origin = span_attributes.get(SentrySpanAttribute.ORIGIN) + + return (op, description, status, http_status, origin) def span_data_for_db_query(span): - # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int]] + # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] span_attributes = span.attributes or {} op = "db" @@ -164,8 +165,9 @@ def span_data_for_db_query(span): statement = cast("Optional[str]", statement) description = statement or span.name + origin = span_attributes.get(SentrySpanAttribute.ORIGIN) - return (op, description, None, None) + return (op, description, None, None, origin) def extract_span_status(span): diff --git a/tests/integrations/opentelemetry/test_utils.py b/tests/integrations/opentelemetry/test_utils.py index ceb58a58ef..2bd95bdc81 100644 --- a/tests/integrations/opentelemetry/test_utils.py +++ b/tests/integrations/opentelemetry/test_utils.py @@ -23,6 +23,7 @@ "description": "OTel Span Blank", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -36,6 +37,7 @@ "description": "OTel Span RPC", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -49,6 +51,7 @@ "description": "OTel Span Messaging", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -62,6 +65,7 @@ "description": "OTel Span FaaS", "status": "ok", "http_status_code": None, + "origin": None, }, ), ], @@ -72,12 +76,13 @@ def test_extract_span_data(name, status, attributes, expected): otel_span.status = Status(StatusCode.UNSET) otel_span.attributes = attributes - op, description, status, http_status_code = extract_span_data(otel_span) + op, description, status, http_status_code, origin = extract_span_data(otel_span) result = { "op": op, "description": description, "status": status, "http_status_code": http_status_code, + "origin": origin, } assert result == expected From 7025038cf9fbb6a6e7b83b43a746a7e209aa5e83 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 13:00:02 +0200 Subject: [PATCH 03/16] xfail compat tests --- tests/new_scopes_compat/test_new_scopes_compat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/new_scopes_compat/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py index 1e109ec036..1daa2b463b 100644 --- a/tests/new_scopes_compat/test_new_scopes_compat.py +++ b/tests/new_scopes_compat/test_new_scopes_compat.py @@ -1,3 +1,5 @@ +import pytest + import sentry_sdk from sentry_sdk.hub import Hub @@ -40,6 +42,7 @@ def test_with_hub_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} +@pytest.mark.xfail("will be removed in 3.0") def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with Hub:` containing a `with configure_scope` block @@ -83,6 +86,7 @@ def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): } +@pytest.mark.xfail("will be removed in 3.0") def test_with_hub_push_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with Hub:` containing a `with push_scope` block @@ -118,6 +122,7 @@ def test_with_hub_push_scope_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "B1": 1, "B5": 1, "Z": 1} +@pytest.mark.xfail("will be removed in 3.0") def test_with_cloned_hub_sdk1(sentry_init, capture_events): """ Mutate data in a `with cloned Hub:` block @@ -147,6 +152,7 @@ def test_with_cloned_hub_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "Z": 1} +@pytest.mark.xfail("will be removed in 3.0") def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with cloned Hub:` containing a `with configure_scope` block @@ -182,6 +188,7 @@ def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "Z": 1} +@pytest.mark.xfail("will be removed in 3.0") def test_with_cloned_hub_push_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with cloned Hub:` containing a `with push_scope` block From 01e1a77f8c37f1aeecdd9eb206e39c7f939e8df2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 13:27:12 +0200 Subject: [PATCH 04/16] profiler stuff --- sentry_sdk/tracing.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 41c998cb99..733e23c329 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1367,11 +1367,16 @@ def set_measurement(self, name, value, unit=""): def set_thread(self, thread_id, thread_name): # type: (Optional[int], Optional[str]) -> None - pass + if thread_id is not None: + self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_data(SPANDATA.THREAD_NAME, thread_name) def set_profiler_id(self, profiler_id): # type: (Optional[str]) -> None - pass + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) def set_http_status(self, http_status): # type: (int) -> None From 723436d043936fd9e3905e483b01f8f15e2eff37 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 14:50:25 +0200 Subject: [PATCH 05/16] fix xfails --- tests/new_scopes_compat/test_new_scopes_compat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/new_scopes_compat/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py index 1daa2b463b..f1e5ef3dfa 100644 --- a/tests/new_scopes_compat/test_new_scopes_compat.py +++ b/tests/new_scopes_compat/test_new_scopes_compat.py @@ -42,7 +42,7 @@ def test_with_hub_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} -@pytest.mark.xfail("will be removed in 3.0") +@pytest.mark.xfail(reason="will be removed in 3.0") def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with Hub:` containing a `with configure_scope` block @@ -86,7 +86,7 @@ def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): } -@pytest.mark.xfail("will be removed in 3.0") +@pytest.mark.xfail(reason="will be removed in 3.0") def test_with_hub_push_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with Hub:` containing a `with push_scope` block @@ -122,7 +122,7 @@ def test_with_hub_push_scope_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "B1": 1, "B5": 1, "Z": 1} -@pytest.mark.xfail("will be removed in 3.0") +@pytest.mark.xfail(reason="will be removed in 3.0") def test_with_cloned_hub_sdk1(sentry_init, capture_events): """ Mutate data in a `with cloned Hub:` block @@ -152,7 +152,7 @@ def test_with_cloned_hub_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "Z": 1} -@pytest.mark.xfail("will be removed in 3.0") +@pytest.mark.xfail(reason="will be removed in 3.0") def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with cloned Hub:` containing a `with configure_scope` block @@ -188,7 +188,7 @@ def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): assert event_z["tags"] == {"A": 1, "Z": 1} -@pytest.mark.xfail("will be removed in 3.0") +@pytest.mark.xfail(reason="will be removed in 3.0") def test_with_cloned_hub_push_scope_sdk1(sentry_init, capture_events): """ Mutate data in a `with cloned Hub:` containing a `with push_scope` block From 392f272b616972da2a519033423b3f997e732a1b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 14:58:18 +0200 Subject: [PATCH 06/16] span_data_for_x fixes --- tests/integrations/opentelemetry/test_utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/integrations/opentelemetry/test_utils.py b/tests/integrations/opentelemetry/test_utils.py index 2bd95bdc81..c9ccfbf6e0 100644 --- a/tests/integrations/opentelemetry/test_utils.py +++ b/tests/integrations/opentelemetry/test_utils.py @@ -171,12 +171,15 @@ def test_span_data_for_http_method(kind, status, attributes, expected): otel_span.status = status otel_span.attributes = attributes - op, description, status, http_status_code = span_data_for_http_method(otel_span) + op, description, status, http_status_code, origin = span_data_for_http_method( + otel_span + ) result = { "op": op, "description": description, "status": status, "http_status_code": http_status_code, + "origin": origin, } assert result == expected @@ -186,19 +189,21 @@ def test_span_data_for_db_query(): otel_span.name = "OTel Span" otel_span.attributes = {} - op, description, status, http_status = span_data_for_db_query(otel_span) + op, description, status, http_status, origin = span_data_for_db_query(otel_span) assert op == "db" assert description == "OTel Span" assert status is None assert http_status is None + assert origin is None otel_span.attributes = {"db.statement": "SELECT * FROM table;"} - op, description, status, http_status = span_data_for_db_query(otel_span) + op, description, status, http_status, origin = span_data_for_db_query(otel_span) assert op == "db" assert description == "SELECT * FROM table;" assert status is None assert http_status is None + assert origin is None @pytest.mark.parametrize( From aef942a588f519b379bbd5e969c18d511355a6d8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 15:47:35 +0200 Subject: [PATCH 07/16] proxy more stuff --- sentry_sdk/tracing.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 733e23c329..90d37763a8 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1306,6 +1306,17 @@ def containing_transaction(self): # type: () -> Optional[Transaction] pass + @property + def trace_id(self): + # type: () -> Optional[str] + return self._otel_span.get_span_context().trace_id + + @property + def sampled(self): + # type: () -> Optional[bool] + # XXX setting this + return self._otel_span.get_span_context().trace_flags.sampled + def start_child(self, **kwargs): # type: (str, **Any) -> POTelSpan pass From dd5b14288f5dd626bf5a376c849d8ba79467b6cb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 16:00:27 +0200 Subject: [PATCH 08/16] small fixes --- sentry_sdk/api.py | 1 - tests/integrations/opentelemetry/test_utils.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 44472f2720..5d71aaa879 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -11,7 +11,6 @@ new_scope, isolation_scope, ) - from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: diff --git a/tests/integrations/opentelemetry/test_utils.py b/tests/integrations/opentelemetry/test_utils.py index c9ccfbf6e0..66ffd7898a 100644 --- a/tests/integrations/opentelemetry/test_utils.py +++ b/tests/integrations/opentelemetry/test_utils.py @@ -104,6 +104,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -118,6 +119,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET /target", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -132,6 +134,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET example.com", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -147,6 +150,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET /target", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -161,6 +165,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET https://example.com/bla/", "status": "ok", "http_status_code": None, + "origin": None, }, ), ], From 4a3c9ba4d080a37938700540555a1dc7946ed839 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 16:17:51 +0200 Subject: [PATCH 09/16] status fixes --- sentry_sdk/tracing.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 90d37763a8..ff6146d6dd 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1370,7 +1370,14 @@ def set_data(self, key, value): def set_status(self, status): # type: (str) -> None - pass + if status == SPANSTATUS.OK: + otel_status = StatusCode.OK + otel_description = "ok" + else: + otel_status = StatusCode.ERROR + otel_description = status.value + + self._otel_span.set_status(otel_status, otel_description) def set_measurement(self, name, value, unit=""): # type: (str, float, MeasurementUnit) -> None @@ -1391,7 +1398,12 @@ def set_profiler_id(self, profiler_id): def set_http_status(self, http_status): # type: (int) -> None - pass + self.set_tag( + "http.status_code", str(http_status) + ) # we keep this for backwards compatibility + # XXX do we still need this? ^ + self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) + self.set_status(get_span_status_from_http_code(http_status)) def is_success(self): # type: () -> bool From 954487e9843432396ddc4bd4789b553b93846990 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 6 Aug 2024 17:09:45 +0200 Subject: [PATCH 10/16] put stuff back on scope --- sentry_sdk/api.py | 8 +++---- sentry_sdk/scope.py | 54 +++++++++++++++++++++++-------------------- sentry_sdk/tracing.py | 41 +++++++++++++++++++++++--------- tests/test_api.py | 4 ++-- 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 5d71aaa879..05ddf4fa98 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,6 +1,6 @@ import inspect -from sentry_sdk import tracing, tracing_utils, Client +from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init from sentry_sdk.tracing import POTelSpan, Transaction, trace from sentry_sdk.crons import monitor @@ -236,10 +236,10 @@ def start_span( ): # type: (...) -> POTelSpan """ - Alias for tracing.POTelSpan constructor. The method signature is the same. + Start and return a span. """ # TODO: Consider adding type hints to the method signature. - return tracing.POTelSpan(**kwargs) + return get_current_scope().start_span(**kwargs) def start_transaction( @@ -281,7 +281,7 @@ def start_transaction( constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ - return start_span(**kwargs) + return get_current_scope().start_span(**kwargs) def set_measurement(name, value, unit=""): diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 342c499ee0..6f82cdf108 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -25,6 +25,7 @@ NoOpSpan, Span, Transaction, + POTelSpan, ) from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( @@ -979,6 +980,10 @@ def start_transaction( ): # type: (Optional[Transaction], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan] """ + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. + Use :py:meth:`sentry_sdk.start_span` instead. + Start and return a transaction. Start an existing transaction if given, otherwise create and start a new @@ -1008,6 +1013,11 @@ def start_transaction( available arguments. """ kwargs.setdefault("scope", self) + span = POTelSpan(**kwargs) + return span + + # XXX + kwargs.setdefault("scope", self) client = self.get_client() @@ -1056,37 +1066,31 @@ def start_span(self, **kwargs): typically used as a context manager to start and stop timing in a `with` block. - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. """ - with new_scope(): - kwargs.setdefault("scope", self) + kwargs.setdefault("scope", self) + span = POTelSpan(**kwargs) - # get current span or transaction - span = self.span or self.get_isolation_scope().span + # with new_scope(): + # kwargs.setdefault("scope", self) - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - if propagation_context is not None: - kwargs["trace_id"] = propagation_context.trace_id + # # get current span or transaction + # span = self.span or self.get_isolation_scope().span - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) + # if span is None: + # # New spans get the `trace_id` from the scope + # if "trace_id" not in kwargs: + # propagation_context = self.get_active_propagation_context() + # if propagation_context is not None: + # kwargs["trace_id"] = propagation_context.trace_id + + # span = Span(**kwargs) + # else: + # # Children take `trace_id`` from the parent span. + # span = span.start_child(**kwargs) - return span + # return span + return span def continue_trace( self, environ_or_headers, op=None, name=None, source=None, origin="manual" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index ff6146d6dd..9537cf38f4 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -36,6 +36,7 @@ R = TypeVar("R") import sentry_sdk.profiler + from sentry_sdk.tracing import Scope from sentry_sdk._types import ( Event, MeasurementUnit, @@ -1259,10 +1260,10 @@ class POTelSpan: def __init__( self, *, - active=True, # type: bool op=None, # type: Optional[str] description=None, # type: Optional[str] origin="manual", # type: str + scope=None, # type: Optional[Scope] **_, # type: dict[str, object] ): # type: (...) -> None @@ -1274,23 +1275,26 @@ def __init__( from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute self._otel_span = tracer.start_span(description or op or "") # XXX - self._active = active self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, origin) if op is not None: - self._otel_span.set_attribute(SentrySpanAttribute.OP, op) + self.op = op + if description is not None: self._otel_span.set_attribute(SentrySpanAttribute.DESCRIPTION, description) + self.scope = scope + def __enter__(self): # type: () -> POTelSpan # XXX use_span? https://github.com/open-telemetry/opentelemetry-python/blob/3836da8543ce9751051e38a110c0468724042e62/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547 # # create a Context object with parent set as current span - if self._active: - ctx = otel_trace.set_span_in_context(self._otel_span) - # set as the implicit current context - self._ctx_token = context.attach(ctx) + ctx = otel_trace.set_span_in_context(self._otel_span) + # set as the implicit current context + self._ctx_token = context.attach(ctx) + scope = self.scope or sentry_sdk.get_current_scope() + scope.span = self return self @@ -1298,8 +1302,7 @@ def __exit__(self, ty, value, tb): # type: (Optional[Any], Optional[Any], Optional[Any]) -> None self._otel_span.end() # XXX set status to error if unset and an exception occurred? - if self._active: - context.detach(self._ctx_token) + context.detach(self._ctx_token) @property def containing_transaction(self): @@ -1314,12 +1317,28 @@ def trace_id(self): @property def sampled(self): # type: () -> Optional[bool] - # XXX setting this return self._otel_span.get_span_context().trace_flags.sampled + @sampled.setter + def sampled(self, value): + pass + + @property + def op(self): + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + self._otel_span.attributes.get(SentrySpanAttribute.OP) + + @op.setter + def op(self, value): + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + self._otel_span.set_attribute(SentrySpanAttribute.OP, value) + def start_child(self, **kwargs): # type: (str, **Any) -> POTelSpan - pass + child_span = POTelSpan(**kwargs) + return child_span @classmethod def continue_from_environ( diff --git a/tests/test_api.py b/tests/test_api.py index ffe1be756d..46fc24fd24 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -30,7 +30,7 @@ def test_get_current_span(): @pytest.mark.forked -def test_get_current_span_default_hub(sentry_init): +def test_get_current_span_current_scope(sentry_init): sentry_init() assert get_current_span() is None @@ -43,7 +43,7 @@ def test_get_current_span_default_hub(sentry_init): @pytest.mark.forked -def test_get_current_span_default_hub_with_transaction(sentry_init): +def test_get_current_span_current_scope_with_transaction(sentry_init): sentry_init() assert get_current_span() is None From 1ef71cd240f128afef81aded58c0e37f83de629e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 7 Aug 2024 13:46:12 +0200 Subject: [PATCH 11/16] some fixes --- sentry_sdk/tracing.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 9537cf38f4..d8ed95ad6c 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -36,7 +36,7 @@ R = TypeVar("R") import sentry_sdk.profiler - from sentry_sdk.tracing import Scope + from sentry_sdk.scope import Scope from sentry_sdk._types import ( Event, MeasurementUnit, @@ -1307,13 +1307,25 @@ def __exit__(self, ty, value, tb): @property def containing_transaction(self): # type: () -> Optional[Transaction] - pass + parent = None + while True: + # XXX + if self._otel_span.parent: + parent = self._otel_span.parent + else: + break + + return parent @property def trace_id(self): # type: () -> Optional[str] return self._otel_span.get_span_context().trace_id + @property + def span_id(self): + return self._otel_span.get_span_context().span_id + @property def sampled(self): # type: () -> Optional[bool] @@ -1337,6 +1349,7 @@ def op(self, value): def start_child(self, **kwargs): # type: (str, **Any) -> POTelSpan + kwargs.setdefault("sampled", self.sampled) child_span = POTelSpan(**kwargs) return child_span @@ -1373,7 +1386,18 @@ def from_traceparent( def to_traceparent(self): # type: () -> str - pass + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent def to_baggage(self): # type: () -> Optional[Baggage] @@ -1391,7 +1415,7 @@ def set_status(self, status): # type: (str) -> None if status == SPANSTATUS.OK: otel_status = StatusCode.OK - otel_description = "ok" + otel_description = None else: otel_status = StatusCode.ERROR otel_description = status.value @@ -1430,7 +1454,14 @@ def is_success(self): def finish(self, scope=None, end_timestamp=None): # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] - pass + # XXX check if already finished + from sentry_sdk.integrations.opentelemetry.utils import convert_otel_timestamp + + if end_timestamp is not None: + end_timestamp = convert_otel_timestamp(end_timestamp) + self._otel_span.end(end_time=end_timestamp) + scope = scope or sentry_sdk.get_current_scope() + maybe_create_breadcrumbs_from_span(scope, self) def to_json(self): # type: () -> dict[str, Any] From 671345555e57e5cbd144a538614fb4751907026d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 7 Aug 2024 15:17:20 +0200 Subject: [PATCH 12/16] wip --- sentry_sdk/api.py | 4 +- .../integrations/opentelemetry/utils.py | 1 + sentry_sdk/integrations/wsgi.py | 6 +- sentry_sdk/scope.py | 13 ++-- sentry_sdk/tracing.py | 78 ++++++++++++++++--- .../integrations/opentelemetry/test_potel.py | 8 +- tests/tracing/test_misc.py | 6 +- 7 files changed, 90 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 05ddf4fa98..2a89f5c4f5 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -281,7 +281,9 @@ def start_transaction( constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ - return get_current_scope().start_span(**kwargs) + return get_current_scope().start_span( + transaction, custom_sampling_context, **kwargs + ) def set_measurement(name, value, unit=""): diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index 4d2e02ef9d..ecb1852404 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -88,6 +88,7 @@ def extract_span_data(span): return (op, description, status, http_status, origin) origin = span.attributes.get(SentrySpanAttribute.ORIGIN) + description = span.attributes.get(SentrySpanAttribute.DESCRIPTION) or description http_method = span.attributes.get(SpanAttributes.HTTP_METHOD) http_method = cast("Optional[str]", http_method) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 7a95611d78..4ac801ea88 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -91,7 +91,7 @@ def __call__(self, environ, start_response): ) ) - transaction = continue_trace( + root_span = continue_trace( environ, op=OP.HTTP_SERVER, name="generic WSGI request", @@ -100,13 +100,13 @@ def __call__(self, environ, start_response): ) with sentry_sdk.start_transaction( - transaction, custom_sampling_context={"wsgi_environ": environ} + root_span, custom_sampling_context={"wsgi_environ": environ} ): try: response = self.app( environ, partial( - _sentry_start_response, start_response, transaction + _sentry_start_response, start_response, root_span ), ) except BaseException: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 6f82cdf108..1af350dad4 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1057,8 +1057,8 @@ def start_transaction( return transaction - def start_span(self, **kwargs): - # type: (Any) -> Span + def start_span(self, span=None, custom_sampling_context=None, **kwargs): + # type: (Optional[POTelSpan], Optional[SamplingContext], Any) -> POTelSpan """ Start a span whose parent is the currently active span or transaction, if any. @@ -1069,7 +1069,8 @@ def start_span(self, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ kwargs.setdefault("scope", self) - span = POTelSpan(**kwargs) + if span is None: + span = POTelSpan(**kwargs) # with new_scope(): # kwargs.setdefault("scope", self) @@ -1095,13 +1096,13 @@ def start_span(self, **kwargs): def continue_trace( self, environ_or_headers, op=None, name=None, source=None, origin="manual" ): - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Transaction + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> POTelSpan """ Sets the propagation context from environment or headers and returns a transaction. """ self.generate_propagation_context(environ_or_headers) - transaction = Transaction.continue_from_headers( + root_span = POTelSpan.continue_from_headers( normalize_incoming_data(environ_or_headers), op=op, origin=origin, @@ -1109,7 +1110,7 @@ def continue_trace( source=source, ) - return transaction + return root_span def capture_event(self, event, hint=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str] diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d8ed95ad6c..9829176111 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1262,8 +1262,10 @@ def __init__( *, op=None, # type: Optional[str] description=None, # type: Optional[str] - origin="manual", # type: str + status=None, # type: Optional[str] scope=None, # type: Optional[Scope] + start_timestamp=None, # type: Optional[Union[datetime, float]] + origin="manual", # type: str **_, # type: dict[str, object] ): # type: (...) -> None @@ -1273,15 +1275,24 @@ def __init__( listed in the signature. These additional arguments are ignored. """ from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + from sentry_sdk.integrations.opentelemetry.utils import convert_otel_timestamp + + if start_timestamp is not None: + start_timestamp = convert_otel_timestamp(start_timestamp) - self._otel_span = tracer.start_span(description or op or "") # XXX + self._otel_span = tracer.start_span( + description or op or "", start_time=start_timestamp + ) # XXX self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, origin) + if op is not None: self.op = op - if description is not None: - self._otel_span.set_attribute(SentrySpanAttribute.DESCRIPTION, description) + self.description = description + + if status is not None: + self.set_status(status) self.scope = scope @@ -1305,8 +1316,37 @@ def __exit__(self, ty, value, tb): context.detach(self._ctx_token) @property - def containing_transaction(self): - # type: () -> Optional[Transaction] + def description(self): + # type: () -> Optional[str] + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + return self._otel_span.attributes.get(SentrySpanAttribute.DESCRIPTION) + + @description.setter + def description(self, value): + # type: (Optional[str]) -> None + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + if value is not None: + self._otel_span.set_attribute(SentrySpanAttribute.DESCRIPTION, value) + + @property + def origin(self): + # type: () -> Optional[str] + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + return self._otel_span.attributes.get(SentrySpanAttribute.ORIGIN) + + @origin.setter + def origin(self, value): + # type: (Optional[str]) -> None + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + if value is not None: + self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, value) + + @property + def root_span(self): parent = None while True: # XXX @@ -1317,6 +1357,21 @@ def containing_transaction(self): return parent + @property + def containing_transaction(self): + # type: () -> Optional[Transaction] + """ + Get the transaction this span is a child of. + + .. deprecated:: 1.0.0 + Use :func:`set_level` instead. + """ + + logger.warning( + "Deprecated: use root_span instead. This will be removed in the future." + ) + return self.root_span + @property def trace_id(self): # type: () -> Optional[str] @@ -1360,7 +1415,9 @@ def continue_from_environ( **kwargs, # type: Any ): # type: (...) -> POTelSpan - pass + # XXX actually propagate + span = POTelSpan(**kwargs) + return span @classmethod def continue_from_headers( @@ -1369,7 +1426,9 @@ def continue_from_headers( **kwargs, # type: Any ): # type: (...) -> POTelSpan - pass + # XXX actually propagate + span = POTelSpan(**kwargs) + return span def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] @@ -1424,7 +1483,8 @@ def set_status(self, status): def set_measurement(self, name, value, unit=""): # type: (str, float, MeasurementUnit) -> None - pass + # XXX own namespace, e.g. sentry.measurement.xxx? + self._otel_span.set_attribute(name, (value, unit)) def set_thread(self, thread_id, thread_name): # type: (Optional[int], Optional[str]) -> None diff --git a/tests/integrations/opentelemetry/test_potel.py b/tests/integrations/opentelemetry/test_potel.py index 2e094b41b5..5e44cc3888 100644 --- a/tests/integrations/opentelemetry/test_potel.py +++ b/tests/integrations/opentelemetry/test_potel.py @@ -41,7 +41,7 @@ def test_root_span_transaction_payload_started_with_otel_only(capture_envelopes) trace_context = contexts["trace"] assert "trace_id" in trace_context assert "span_id" in trace_context - assert trace_context["origin"] == "auto.otel" + assert trace_context["origin"] == "manual" assert trace_context["op"] == "request" assert trace_context["status"] == "ok" @@ -62,7 +62,7 @@ def test_child_span_payload_started_with_otel_only(capture_envelopes): assert span["op"] == "db" assert span["description"] == "db" - assert span["origin"] == "auto.otel" + assert span["origin"] == "manual" assert span["status"] == "ok" assert span["span_id"] is not None assert span["trace_id"] == payload["contexts"]["trace"]["trace_id"] @@ -124,7 +124,7 @@ def test_root_span_transaction_payload_started_with_sentry_only(capture_envelope trace_context = contexts["trace"] assert "trace_id" in trace_context assert "span_id" in trace_context - assert trace_context["origin"] == "auto.otel" + assert trace_context["origin"] == "manual" assert trace_context["op"] == "request" assert trace_context["status"] == "ok" @@ -145,7 +145,7 @@ def test_child_span_payload_started_with_sentry_only(capture_envelopes): assert span["op"] == "db" assert span["description"] == "db" - assert span["origin"] == "auto.otel" + assert span["origin"] == "manual" assert span["status"] == "ok" assert span["span_id"] is not None assert span["trace_id"] == payload["contexts"]["trace"]["trace_id"] diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index 02966642fd..996d9c4d5d 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -258,7 +258,7 @@ def test_circular_references(monkeypatch, sentry_init, request): assert gc.collect() == 0 -def test_set_meaurement(sentry_init, capture_events): +def test_set_measurement(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0) events = capture_events() @@ -286,7 +286,7 @@ def test_set_meaurement(sentry_init, capture_events): assert event["measurements"]["metric.foobar"] == {"value": 17.99, "unit": "percent"} -def test_set_meaurement_public_api(sentry_init, capture_events): +def test_set_measurement_public_api(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0) events = capture_events() @@ -412,7 +412,7 @@ def test_transaction_dropped_debug_not_started(sentry_init, sampled): ) -def test_transaction_dropeed_sampled_false(sentry_init): +def test_transaction_dropped_sampled_false(sentry_init): sentry_init(enable_tracing=True) tx = Transaction(sampled=False) From 7af34593f409a7750b647cecb9eb429d59ca7ba2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 7 Aug 2024 17:24:32 +0200 Subject: [PATCH 13/16] very much wip --- sentry_sdk/api.py | 18 +- sentry_sdk/integrations/boto3.py | 3 +- sentry_sdk/scope.py | 175 +- sentry_sdk/tracing.py | 2110 +++++++++-------- sentry_sdk/tracing_utils.py | 5 +- tests/new_scopes_compat/__init__.py | 7 - tests/new_scopes_compat/conftest.py | 8 - .../test_new_scopes_compat.py | 224 -- 8 files changed, 1161 insertions(+), 1389 deletions(-) delete mode 100644 tests/new_scopes_compat/__init__.py delete mode 100644 tests/new_scopes_compat/conftest.py delete mode 100644 tests/new_scopes_compat/test_new_scopes_compat.py diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 2a89f5c4f5..33f443b120 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -2,7 +2,7 @@ from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init -from sentry_sdk.tracing import POTelSpan, Transaction, trace +from sentry_sdk.tracing import Span, trace from sentry_sdk.crons import monitor # TODO-neel-potel make 2 scope strategies/impls and switch @@ -36,7 +36,7 @@ LogLevelStr, SamplingContext, ) - from sentry_sdk.tracing import Span, TransactionKwargs + from sentry_sdk.tracing import TransactionKwargs T = TypeVar("T") F = TypeVar("F", bound=Callable[..., Any]) @@ -232,22 +232,24 @@ def flush( def start_span( + root_span=None, + custom_sampling_context=None, **kwargs, # type: Any ): - # type: (...) -> POTelSpan + # type: (...) -> Span """ Start and return a span. """ # TODO: Consider adding type hints to the method signature. - return get_current_scope().start_span(**kwargs) + return get_current_scope().start_span(root_span, custom_sampling_context, **kwargs) def start_transaction( - transaction=None, # type: Optional[Transaction] + transaction=None, # type: Optional[Span] custom_sampling_context=None, # type: Optional[SamplingContext] **kwargs, # type: Unpack[TransactionKwargs] ): - # type: (...) -> POTelSpan + # type: (...) -> Span """ .. deprecated:: 3.0.0 This function is deprecated and will be removed in a future release. @@ -282,7 +284,7 @@ def start_transaction( available arguments. """ return get_current_scope().start_span( - transaction, custom_sampling_context, **kwargs + root_span=transaction, custom_sampling_context=custom_sampling_context, **kwargs ) @@ -324,7 +326,7 @@ def get_baggage(): def continue_trace( environ_or_headers, op=None, name=None, source=None, origin="manual" ): - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Transaction + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Span """ Sets the propagation context from environment or headers and returns a transaction. """ diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index 0fb997767b..3c5131e9d0 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -3,7 +3,6 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import Integration, DidNotEnable -from sentry_sdk.tracing import Span from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( @@ -19,6 +18,8 @@ from typing import Optional from typing import Type + from sentry_sdk.tracing import Span + try: from botocore import __version__ as BOTOCORE_VERSION # type: ignore from botocore.client import BaseClient # type: ignore diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 1af350dad4..f146196082 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -22,10 +22,7 @@ from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, - NoOpSpan, Span, - Transaction, - POTelSpan, ) from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( @@ -169,10 +166,8 @@ class Scope(object): "_level", "_name", "_fingerprint", - # note that for legacy reasons, _transaction is the transaction *name*, - # not a Transaction object (the object is stored in _span) - "_transaction", - "_transaction_info", + "_root_span_name", + "_root_span_info", "_user", "_tags", "_contexts", @@ -224,8 +219,8 @@ def __copy__(self): rv._level = self._level rv._name = self._name rv._fingerprint = self._fingerprint - rv._transaction = self._transaction - rv._transaction_info = dict(self._transaction_info) + rv._root_span_name = self._root_span_name + rv._root_span_info = dict(self._root_span_info) rv._user = self._user rv._tags = dict(self._tags) @@ -675,8 +670,8 @@ def clear(self): """Clears the entire scope.""" self._level = None # type: Optional[LogLevelStr] self._fingerprint = None # type: Optional[List[str]] - self._transaction = None # type: Optional[str] - self._transaction_info = {} # type: MutableMapping[str, str] + self._root_span_name = None # type: Optional[str] + self._root_span_info = {} # type: MutableMapping[str, str] self._user = None # type: Optional[Dict[str, Any]] self._tags = {} # type: Dict[str, Any] @@ -698,23 +693,6 @@ def clear(self): # self._last_event_id is only applicable to isolation scopes self._last_event_id = None # type: Optional[str] - @_attr_setter - def level(self, value): - # type: (LogLevelStr) -> None - """ - When set this overrides the level. - - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. - - :param value: The level to set. - """ - logger.warning( - "Deprecated: use .set_level() instead. This will be removed in the future." - ) - - self._level = value - def set_level(self, value): # type: (LogLevelStr) -> None """ @@ -733,58 +711,54 @@ def fingerprint(self, value): @property def transaction(self): # type: () -> Any - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" + # would be type: () -> Optional[Span], see https://github.com/python/mypy/issues/3004 + """ + Return the transaction in the scope, if any. + + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. Use Scope.root_span instead. + """ + + logger.warning( + "Deprecated: use Scope.root_span instead. This will be removed in the future." + ) + return self.root_span + + @property + def root_span(self): + """Return the root span in the scope, if any.""" # there is no span/transaction on the scope if self._span is None: return None # there is an orphan span on the scope - if self._span.containing_transaction is None: + if self._span.root_span is None: return None - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value): - # type: (Any) -> None - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span and self._span.containing_transaction: - self._span.containing_transaction.name = value + # there is either a root span (which is its own containing + # root span) or a non-orphan span on the scope + return self._span.root_span def set_transaction_name(self, name, source=None): # type: (str, Optional[str]) -> None - """Set the transaction name and optionally the transaction source.""" - self._transaction = name + """ + Set the transaction name and optionally the transaction source. - if self._span and self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. Use Scope.set_root_span_name instead. + """ + self.set_root_span_name(name, source) + def set_root_span_name(self, name, source=None): + """Set the root span name and optionally the source.""" + self._root_span_name = name + if self._span and self._span.root_span: + self._span.root_span.name = name + if source: + self._span.root_span.source = source if source: - self._transaction_info["source"] = source + self._root_span_info["source"] = source @_attr_setter def user(self, value): @@ -803,21 +777,22 @@ def set_user(self, value): @property def span(self): # type: () -> Optional[Span] - """Get/set current tracing span or transaction.""" + """Get current tracing span.""" return self._span @span.setter def span(self, span): + """Set current tracing span.""" # type: (Optional[Span]) -> None self._span = span # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source + # does not set root span name. + return + if span.is_root_span: + if span.name: + self._root_span_name = span.name + if span.source: + self._root_span_info["source"] = span.source @property def profile(self): @@ -978,7 +953,7 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): def start_transaction( self, transaction=None, custom_sampling_context=None, **kwargs ): - # type: (Optional[Transaction], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan] + # type: (Optional[Span], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Span """ .. deprecated:: 3.0.0 This function is deprecated and will be removed in a future release. @@ -1013,7 +988,7 @@ def start_transaction( available arguments. """ kwargs.setdefault("scope", self) - span = POTelSpan(**kwargs) + span = Span(**kwargs) return span # XXX @@ -1057,8 +1032,8 @@ def start_transaction( return transaction - def start_span(self, span=None, custom_sampling_context=None, **kwargs): - # type: (Optional[POTelSpan], Optional[SamplingContext], Any) -> POTelSpan + def start_span(self, root_span=None, custom_sampling_context=None, **kwargs): + # type: (Optional[Span], Optional[SamplingContext], Any) -> Span """ Start a span whose parent is the currently active span or transaction, if any. @@ -1069,9 +1044,22 @@ def start_span(self, span=None, custom_sampling_context=None, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ kwargs.setdefault("scope", self) + if root_span: + return root_span + + span = self.span or self.get_isolation_scope().span + if span is None: - span = POTelSpan(**kwargs) + if "trace_id" not in kwargs: + propagation_context = self.get_active_propagation_context() + if propagation_context is not None: + kwargs["trace_id"] = propagation_context.trace_id + return Span(**kwargs) + else: + return span.start_child(**kwargs) + + # XXX # with new_scope(): # kwargs.setdefault("scope", self) @@ -1091,18 +1079,17 @@ def start_span(self, span=None, custom_sampling_context=None, **kwargs): # span = span.start_child(**kwargs) # return span - return span def continue_trace( self, environ_or_headers, op=None, name=None, source=None, origin="manual" ): - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> POTelSpan + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Span """ Sets the propagation context from environment or headers and returns a transaction. """ self.generate_propagation_context(environ_or_headers) - root_span = POTelSpan.continue_from_headers( + root_span = Span.continue_from_headers( normalize_incoming_data(environ_or_headers), op=op, origin=origin, @@ -1314,15 +1301,15 @@ def _apply_user_to_event(self, event, hint, options): if event.get("user") is None and self._user is not None: event["user"] = self._user - def _apply_transaction_name_to_event(self, event, hint, options): + def _apply_root_span_name_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction") is None and self._transaction is not None: - event["transaction"] = self._transaction + if event.get("transaction") is None and self._root_span_name is not None: + event["transaction"] = self._root_span_name - def _apply_transaction_info_to_event(self, event, hint, options): + def _apply_root_span_info_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction_info") is None and self._transaction_info is not None: - event["transaction_info"] = self._transaction_info + if event.get("transaction_info") is None and self._root_span_info is not None: + event["transaction_info"] = self._root_span_info def _apply_fingerprint_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None @@ -1444,8 +1431,8 @@ def apply_to_event( self._apply_level_to_event(event, hint, options) self._apply_fingerprint_to_event(event, hint, options) self._apply_user_to_event(event, hint, options) - self._apply_transaction_name_to_event(event, hint, options) - self._apply_transaction_info_to_event(event, hint, options) + self._apply_root_span_name_to_event(event, hint, options) + self._apply_root_span_info_to_event(event, hint, options) self._apply_tags_to_event(event, hint, options) self._apply_extra_to_event(event, hint, options) @@ -1469,10 +1456,10 @@ def update_from_scope(self, scope): self._level = scope._level if scope._fingerprint is not None: self._fingerprint = scope._fingerprint - if scope._transaction is not None: - self._transaction = scope._transaction - if scope._transaction_info is not None: - self._transaction_info.update(scope._transaction_info) + if scope._root_span_name is not None: + self._root_span_name = scope._root_span_name + if scope._root_span_info is not None: + self._root_span_info.update(scope._root_span_info) if scope._user is not None: self._user = scope._user if scope._tags: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 9829176111..e540b4339f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -218,1044 +218,1033 @@ def add(self, span): self.spans.append(span) -class Span: - """A span holds timing information of a block of code. - Spans can have multiple child spans thus forming a span tree. - - :param trace_id: The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - :param span_id: The span ID of this span. If omitted, a new span ID will be generated. - :param parent_span_id: The span ID of the parent span, if applicable. - :param same_process_as_parent: Whether this span is in the same process as the parent span. - :param sampled: Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - :param op: The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - :param description: A description of what operation is being performed within the span. - :param hub: The hub to use for this span. - - .. deprecated:: 2.0.0 - Please use the `scope` parameter, instead. - :param status: The span's status. Possible values are listed at - https://develop.sentry.dev/sdk/event-payloads/span/ - :param containing_transaction: The transaction that this span belongs to. - :param start_timestamp: The timestamp when the span started. If omitted, the current time - will be used. - :param scope: The scope to use for this span. If not provided, we use the current scope. - """ - - __slots__ = ( - "trace_id", - "span_id", - "parent_span_id", - "same_process_as_parent", - "sampled", - "op", - "description", - "_measurements", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "hub", - "_context_manager_state", - "_containing_transaction", - "_local_aggregator", - "scope", - "origin", - ) - - def __init__( - self, - trace_id=None, # type: Optional[str] - span_id=None, # type: Optional[str] - parent_span_id=None, # type: Optional[str] - same_process_as_parent=True, # type: bool - sampled=None, # type: Optional[bool] - op=None, # type: Optional[str] - description=None, # type: Optional[str] - hub=None, # type: Optional[sentry_sdk.Hub] # deprecated - status=None, # type: Optional[str] - containing_transaction=None, # type: Optional[Transaction] - start_timestamp=None, # type: Optional[Union[datetime, float]] - scope=None, # type: Optional[sentry_sdk.Scope] - origin="manual", # type: str - ): - # type: (...) -> None - self.trace_id = trace_id or uuid.uuid4().hex - self.span_id = span_id or uuid.uuid4().hex[16:] - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = description - self.status = status - self.hub = hub # backwards compatibility - self.scope = scope - self.origin = origin - self._measurements = {} # type: Dict[str, MeasurementValue] - self._tags = {} # type: MutableMapping[str, str] - self._data = {} # type: Dict[str, Any] - self._containing_transaction = containing_transaction - - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - - if start_timestamp is None: - start_timestamp = datetime.now(timezone.utc) - elif isinstance(start_timestamp, float): - start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) - self.start_timestamp = start_timestamp - try: - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp = None # type: Optional[datetime] - - self._span_recorder = None # type: Optional[_SpanRecorder] - self._local_aggregator = None # type: Optional[LocalAggregator] - - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - self.set_profiler_id(get_profiler_id()) - - # TODO this should really live on the Transaction class rather than the Span - # class - def init_span_recorder(self, maxlen): - # type: (int) -> None - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - def _get_local_aggregator(self): - # type: (...) -> LocalAggregator - rv = self._local_aggregator - if rv is None: - rv = self._local_aggregator = LocalAggregator() - return rv - - def __repr__(self): - # type: () -> str - return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.op, - self.description, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.origin, - ) - ) - - def __enter__(self): - # type: () -> Span - scope = self.scope or sentry_sdk.get_current_scope() - old_span = scope.span - scope.span = self - self._context_manager_state = (scope, old_span) - return self - - def __exit__(self, ty, value, tb): - # type: (Optional[Any], Optional[Any], Optional[Any]) -> None - if value is not None: - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - scope, old_span = self._context_manager_state - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self): - # type: () -> Optional[Transaction] - """The ``Transaction`` that this span belongs to. - The ``Transaction`` is the root of the span tree, - so one could also think of this ``Transaction`` as the "root span".""" - - # this is a getter rather than a regular attribute so that transactions - # can return `self` here instead (as a way to prevent them circularly - # referencing themselves) - return self._containing_transaction - - def start_child(self, **kwargs): - # type: (**Any) -> Span - """ - Start a sub-span from the current span or transaction. - - Takes the same arguments as the initializer of :py:class:`Span`. The - trace id, sampling decision, transaction pointer, and span recorder are - inherited from the current span/transaction. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. - """ - kwargs.setdefault("sampled", self.sampled) +# class Span: +# """A span holds timing information of a block of code. +# Spans can have multiple child spans thus forming a span tree. + +# :param trace_id: The trace ID of the root span. If this new span is to be the root span, +# omit this parameter, and a new trace ID will be generated. +# :param span_id: The span ID of this span. If omitted, a new span ID will be generated. +# :param parent_span_id: The span ID of the parent span, if applicable. +# :param same_process_as_parent: Whether this span is in the same process as the parent span. +# :param sampled: Whether the span should be sampled. Overrides the default sampling decision +# for this span when provided. +# :param op: The span's operation. A list of recommended values is available here: +# https://develop.sentry.dev/sdk/performance/span-operations/ +# :param description: A description of what operation is being performed within the span. +# :param hub: The hub to use for this span. + +# .. deprecated:: 2.0.0 +# Please use the `scope` parameter, instead. +# :param status: The span's status. Possible values are listed at +# https://develop.sentry.dev/sdk/event-payloads/span/ +# :param containing_transaction: The transaction that this span belongs to. +# :param start_timestamp: The timestamp when the span started. If omitted, the current time +# will be used. +# :param scope: The scope to use for this span. If not provided, we use the current scope. +# """ + +# __slots__ = ( +# "trace_id", +# "span_id", +# "parent_span_id", +# "same_process_as_parent", +# "sampled", +# "op", +# "description", +# "_measurements", +# "start_timestamp", +# "_start_timestamp_monotonic_ns", +# "status", +# "timestamp", +# "_tags", +# "_data", +# "_span_recorder", +# "hub", +# "_context_manager_state", +# "_containing_transaction", +# "_local_aggregator", +# "scope", +# "origin", +# ) + +# def __init__( +# self, +# trace_id=None, # type: Optional[str] +# span_id=None, # type: Optional[str] +# parent_span_id=None, # type: Optional[str] +# same_process_as_parent=True, # type: bool +# sampled=None, # type: Optional[bool] +# op=None, # type: Optional[str] +# description=None, # type: Optional[str] +# hub=None, # type: Optional[sentry_sdk.Hub] # deprecated +# status=None, # type: Optional[str] +# containing_transaction=None, # type: Optional[Transaction] +# start_timestamp=None, # type: Optional[Union[datetime, float]] +# scope=None, # type: Optional[sentry_sdk.Scope] +# origin="manual", # type: str +# ): +# # type: (...) -> None +# self.trace_id = trace_id or uuid.uuid4().hex +# self.span_id = span_id or uuid.uuid4().hex[16:] +# self.parent_span_id = parent_span_id +# self.same_process_as_parent = same_process_as_parent +# self.sampled = sampled +# self.op = op +# self.description = description +# self.status = status +# self.hub = hub # backwards compatibility +# self.scope = scope +# self.origin = origin +# self._measurements = {} # type: Dict[str, MeasurementValue] +# self._tags = {} # type: MutableMapping[str, str] +# self._data = {} # type: Dict[str, Any] +# self._containing_transaction = containing_transaction + +# if hub is not None: +# warnings.warn( +# "The `hub` parameter is deprecated. Please use `scope` instead.", +# DeprecationWarning, +# stacklevel=2, +# ) + +# self.scope = self.scope or hub.scope + +# if start_timestamp is None: +# start_timestamp = datetime.now(timezone.utc) +# elif isinstance(start_timestamp, float): +# start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) +# self.start_timestamp = start_timestamp +# try: +# # profiling depends on this value and requires that +# # it is measured in nanoseconds +# self._start_timestamp_monotonic_ns = nanosecond_time() +# except AttributeError: +# pass + +# #: End timestamp of span +# self.timestamp = None # type: Optional[datetime] + +# self._span_recorder = None # type: Optional[_SpanRecorder] +# self._local_aggregator = None # type: Optional[LocalAggregator] + +# thread_id, thread_name = get_current_thread_meta() +# self.set_thread(thread_id, thread_name) +# self.set_profiler_id(get_profiler_id()) + +# # TODO this should really live on the Transaction class rather than the Span +# # class +# def init_span_recorder(self, maxlen): +# # type: (int) -> None +# if self._span_recorder is None: +# self._span_recorder = _SpanRecorder(maxlen) + +# def _get_local_aggregator(self): +# # type: (...) -> LocalAggregator +# rv = self._local_aggregator +# if rv is None: +# rv = self._local_aggregator = LocalAggregator() +# return rv + +# def __repr__(self): +# # type: () -> str +# return ( +# "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" +# % ( +# self.__class__.__name__, +# self.op, +# self.description, +# self.trace_id, +# self.span_id, +# self.parent_span_id, +# self.sampled, +# self.origin, +# ) +# ) + +# def __enter__(self): +# # type: () -> Span +# scope = self.scope or sentry_sdk.get_current_scope() +# old_span = scope.span +# scope.span = self +# self._context_manager_state = (scope, old_span) +# return self + +# def __exit__(self, ty, value, tb): +# # type: (Optional[Any], Optional[Any], Optional[Any]) -> None +# if value is not None: +# self.set_status(SPANSTATUS.INTERNAL_ERROR) + +# scope, old_span = self._context_manager_state +# del self._context_manager_state +# self.finish(scope) +# scope.span = old_span + +# @property +# def containing_transaction(self): +# # type: () -> Optional[Transaction] +# """The ``Transaction`` that this span belongs to. +# The ``Transaction`` is the root of the span tree, +# so one could also think of this ``Transaction`` as the "root span".""" + +# # this is a getter rather than a regular attribute so that transactions +# # can return `self` here instead (as a way to prevent them circularly +# # referencing themselves) +# return self._containing_transaction + +# def start_child(self, **kwargs): +# # type: (**Any) -> Span +# """ +# Start a sub-span from the current span or transaction. + +# Takes the same arguments as the initializer of :py:class:`Span`. The +# trace id, sampling decision, transaction pointer, and span recorder are +# inherited from the current span/transaction. + +# The instrumenter parameter is deprecated for user code, and it will +# be removed in the next major version. Going forward, it should only +# be used by the SDK itself. +# """ +# kwargs.setdefault("sampled", self.sampled) + +# child = Span( +# trace_id=self.trace_id, +# parent_span_id=self.span_id, +# containing_transaction=self.containing_transaction, +# **kwargs, +# ) + +# span_recorder = ( +# self.containing_transaction and self.containing_transaction._span_recorder +# ) +# if span_recorder: +# span_recorder.add(child) + +# return child + +# @classmethod +# def continue_from_environ( +# cls, +# environ, # type: Mapping[str, str] +# **kwargs, # type: Any +# ): +# # type: (...) -> Transaction +# """ +# Create a Transaction with the given params, then add in data pulled from +# the ``sentry-trace`` and ``baggage`` headers from the environ (if any) +# before returning the Transaction. + +# This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` +# in that it assumes header names in the form ``HTTP_HEADER_NAME`` - +# such as you would get from a WSGI/ASGI environ - +# rather than the form ``header-name``. + +# :param environ: The ASGI/WSGI environ to pull information from. +# """ +# if cls is Span: +# logger.warning( +# "Deprecated: use Transaction.continue_from_environ " +# "instead of Span.continue_from_environ." +# ) +# return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) + +# @classmethod +# def continue_from_headers( +# cls, +# headers, # type: Mapping[str, str] +# **kwargs, # type: Any +# ): +# # type: (...) -> Transaction +# """ +# Create a transaction with the given params (including any data pulled from +# the ``sentry-trace`` and ``baggage`` headers). + +# :param headers: The dictionary with the HTTP headers to pull information from. +# """ +# # TODO move this to the Transaction class +# if cls is Span: +# logger.warning( +# "Deprecated: use Transaction.continue_from_headers " +# "instead of Span.continue_from_headers." +# ) + +# # TODO-neel move away from this kwargs stuff, it's confusing and opaque +# # make more explicit +# baggage = Baggage.from_incoming_header(headers.get(BAGGAGE_HEADER_NAME)) +# kwargs.update({BAGGAGE_HEADER_NAME: baggage}) + +# sentrytrace_kwargs = extract_sentrytrace_data( +# headers.get(SENTRY_TRACE_HEADER_NAME) +# ) + +# if sentrytrace_kwargs is not None: +# kwargs.update(sentrytrace_kwargs) + +# # If there's an incoming sentry-trace but no incoming baggage header, +# # for instance in traces coming from older SDKs, +# # baggage will be empty and immutable and won't be populated as head SDK. +# baggage.freeze() + +# transaction = Transaction(**kwargs) +# transaction.same_process_as_parent = False + +# return transaction + +# def iter_headers(self): +# # type: () -> Iterator[Tuple[str, str]] +# """ +# Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. +# If the span's containing transaction doesn't yet have a ``baggage`` value, +# this will cause one to be generated and stored. +# """ +# if not self.containing_transaction: +# # Do not propagate headers if there is no containing transaction. Otherwise, this +# # span ends up being the root span of a new trace, and since it does not get sent +# # to Sentry, the trace will be missing a root transaction. The dynamic sampling +# # context will also be missing, breaking dynamic sampling & traces. +# return + +# yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + +# baggage = self.containing_transaction.get_baggage().serialize() +# if baggage: +# yield BAGGAGE_HEADER_NAME, baggage + +# @classmethod +# def from_traceparent( +# cls, +# traceparent, # type: Optional[str] +# **kwargs, # type: Any +# ): +# # type: (...) -> Optional[Transaction] +# """ +# DEPRECATED: Use :py:meth:`sentry_sdk.tracing.Span.continue_from_headers`. + +# Create a ``Transaction`` with the given params, then add in data pulled from +# the given ``sentry-trace`` header value before returning the ``Transaction``. +# """ +# logger.warning( +# "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) " +# "instead of from_traceparent(traceparent, **kwargs)" +# ) + +# if not traceparent: +# return None + +# return cls.continue_from_headers( +# {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs +# ) + +# def to_traceparent(self): +# # type: () -> str +# if self.sampled is True: +# sampled = "1" +# elif self.sampled is False: +# sampled = "0" +# else: +# sampled = None + +# traceparent = "%s-%s" % (self.trace_id, self.span_id) +# if sampled is not None: +# traceparent += "-%s" % (sampled,) + +# return traceparent + +# def to_baggage(self): +# # type: () -> Optional[Baggage] +# """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` +# associated with this ``Span``, if any. (Taken from the root of the span tree.) +# """ +# if self.containing_transaction: +# return self.containing_transaction.get_baggage() +# return None + +# def set_tag(self, key, value): +# # type: (str, Any) -> None +# self._tags[key] = value + +# def set_data(self, key, value): +# # type: (str, Any) -> None +# self._data[key] = value + +# def set_status(self, value): +# # type: (str) -> None +# self.status = value + +# def set_measurement(self, name, value, unit=""): +# # type: (str, float, MeasurementUnit) -> None +# self._measurements[name] = {"value": value, "unit": unit} + +# def set_thread(self, thread_id, thread_name): +# # type: (Optional[int], Optional[str]) -> None + +# if thread_id is not None: +# self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + +# if thread_name is not None: +# self.set_data(SPANDATA.THREAD_NAME, thread_name) + +# def set_profiler_id(self, profiler_id): +# # type: (Optional[str]) -> None +# if profiler_id is not None: +# self.set_data(SPANDATA.PROFILER_ID, profiler_id) + +# def set_http_status(self, http_status): +# # type: (int) -> None +# self.set_tag( +# "http.status_code", str(http_status) +# ) # we keep this for backwards compatibility +# self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) +# self.set_status(get_span_status_from_http_code(http_status)) + +# def is_success(self): +# # type: () -> bool +# return self.status == "ok" + +# def finish(self, scope=None, end_timestamp=None): +# # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] +# """ +# Sets the end timestamp of the span. + +# Additionally it also creates a breadcrumb from the span, +# if the span represents a database or HTTP request. + +# :param scope: The scope to use for this transaction. +# If not provided, the current scope will be used. +# :param end_timestamp: Optional timestamp that should +# be used as timestamp instead of the current time. + +# :return: Always ``None``. The type is ``Optional[str]`` to match +# the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. +# """ +# if self.timestamp is not None: +# # This span is already finished, ignore. +# return None + +# try: +# if end_timestamp: +# if isinstance(end_timestamp, float): +# end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) +# self.timestamp = end_timestamp +# else: +# elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns +# self.timestamp = self.start_timestamp + timedelta( +# microseconds=elapsed / 1000 +# ) +# except AttributeError: +# self.timestamp = datetime.now(timezone.utc) + +# scope = scope or sentry_sdk.get_current_scope() +# maybe_create_breadcrumbs_from_span(scope, self) + +# return None + +# def to_json(self): +# # type: () -> Dict[str, Any] +# """Returns a JSON-compatible representation of the span.""" + +# rv = { +# "trace_id": self.trace_id, +# "span_id": self.span_id, +# "parent_span_id": self.parent_span_id, +# "same_process_as_parent": self.same_process_as_parent, +# "op": self.op, +# "description": self.description, +# "start_timestamp": self.start_timestamp, +# "timestamp": self.timestamp, +# "origin": self.origin, +# } # type: Dict[str, Any] + +# if self.status: +# self._tags["status"] = self.status + +# if self._local_aggregator is not None: +# metrics_summary = self._local_aggregator.to_json() +# if metrics_summary: +# rv["_metrics_summary"] = metrics_summary + +# if len(self._measurements) > 0: +# rv["measurements"] = self._measurements + +# tags = self._tags +# if tags: +# rv["tags"] = tags + +# data = self._data +# if data: +# rv["data"] = data + +# return rv + +# def get_trace_context(self): +# # type: () -> Any +# rv = { +# "trace_id": self.trace_id, +# "span_id": self.span_id, +# "parent_span_id": self.parent_span_id, +# "op": self.op, +# "description": self.description, +# "origin": self.origin, +# } # type: Dict[str, Any] +# if self.status: +# rv["status"] = self.status + +# if self.containing_transaction: +# rv["dynamic_sampling_context"] = ( +# self.containing_transaction.get_baggage().dynamic_sampling_context() +# ) + +# data = {} + +# thread_id = self._data.get(SPANDATA.THREAD_ID) +# if thread_id is not None: +# data["thread.id"] = thread_id + +# thread_name = self._data.get(SPANDATA.THREAD_NAME) +# if thread_name is not None: +# data["thread.name"] = thread_name + +# if data: +# rv["data"] = data + +# return rv + +# def get_profile_context(self): +# # type: () -> Optional[ProfileContext] +# profiler_id = self._data.get(SPANDATA.PROFILER_ID) +# if profiler_id is None: +# return None + +# return { +# "profiler_id": profiler_id, +# } + + +# class Transaction(Span): +# """The Transaction is the root element that holds all the spans +# for Sentry performance instrumentation. + +# :param name: Identifier of the transaction. +# Will show up in the Sentry UI. +# :param parent_sampled: Whether the parent transaction was sampled. +# If True this transaction will be kept, if False it will be discarded. +# :param baggage: The W3C baggage header value. +# (see https://www.w3.org/TR/baggage/) +# :param source: A string describing the source of the transaction name. +# This will be used to determine the transaction's type. +# See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations +# for more information. Default "custom". +# :param kwargs: Additional arguments to be passed to the Span constructor. +# See :py:class:`sentry_sdk.tracing.Span` for available arguments. +# """ + +# __slots__ = ( +# "name", +# "source", +# "parent_sampled", +# # used to create baggage value for head SDKs in dynamic sampling +# "sample_rate", +# "_measurements", +# "_contexts", +# "_profile", +# "_baggage", +# ) + +# def __init__( +# self, +# name="", # type: str +# parent_sampled=None, # type: Optional[bool] +# baggage=None, # type: Optional[Baggage] +# source=TRANSACTION_SOURCE_CUSTOM, # type: str +# **kwargs, # type: Unpack[SpanKwargs] +# ): +# # type: (...) -> None + +# super().__init__(**kwargs) + +# self.name = name +# self.source = source +# self.sample_rate = None # type: Optional[float] +# self.parent_sampled = parent_sampled +# self._measurements = {} # type: Dict[str, MeasurementValue] +# self._contexts = {} # type: Dict[str, Any] +# self._profile = ( +# None +# ) # type: Optional[sentry_sdk.profiler.transaction_profiler.Profile] +# self._baggage = baggage + +# def __repr__(self): +# # type: () -> str +# return ( +# "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" +# % ( +# self.__class__.__name__, +# self.name, +# self.op, +# self.trace_id, +# self.span_id, +# self.parent_span_id, +# self.sampled, +# self.source, +# self.origin, +# ) +# ) + +# def _possibly_started(self): +# # type: () -> bool +# """Returns whether the transaction might have been started. + +# If this returns False, we know that the transaction was not started +# with sentry_sdk.start_transaction, and therefore the transaction will +# be discarded. +# """ + +# # We must explicitly check self.sampled is False since self.sampled can be None +# return self._span_recorder is not None or self.sampled is False + +# def __enter__(self): +# # type: () -> Transaction +# if not self._possibly_started(): +# logger.debug( +# "Transaction was entered without being started with sentry_sdk.start_transaction." +# "The transaction will not be sent to Sentry. To fix, start the transaction by" +# "passing it to sentry_sdk.start_transaction." +# ) + +# super().__enter__() + +# if self._profile is not None: +# self._profile.__enter__() + +# return self + +# def __exit__(self, ty, value, tb): +# # type: (Optional[Any], Optional[Any], Optional[Any]) -> None +# if self._profile is not None: +# self._profile.__exit__(ty, value, tb) + +# super().__exit__(ty, value, tb) + +# @property +# def containing_transaction(self): +# # type: () -> Transaction +# """The root element of the span tree. +# In the case of a transaction it is the transaction itself. +# """ + +# # Transactions (as spans) belong to themselves (as transactions). This +# # is a getter rather than a regular attribute to avoid having a circular +# # reference. +# return self + +# def _get_scope_from_finish_args( +# self, +# scope_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] +# hub_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] +# ): +# # type: (...) -> Optional[sentry_sdk.Scope] +# """ +# Logic to get the scope from the arguments passed to finish. This +# function exists for backwards compatibility with the old finish. + +# TODO: Remove this function in the next major version. +# """ +# scope_or_hub = scope_arg +# if hub_arg is not None: +# warnings.warn( +# "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", +# DeprecationWarning, +# stacklevel=3, +# ) + +# scope_or_hub = hub_arg + +# if isinstance(scope_or_hub, sentry_sdk.Hub): +# warnings.warn( +# "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", +# DeprecationWarning, +# stacklevel=3, +# ) + +# return scope_or_hub.scope + +# return scope_or_hub + +# def finish( +# self, +# scope=None, # type: Optional[sentry_sdk.Scope] +# end_timestamp=None, # type: Optional[Union[float, datetime]] +# *, +# hub=None, # type: Optional[sentry_sdk.Hub] +# ): +# # type: (...) -> Optional[str] +# """Finishes the transaction and sends it to Sentry. +# All finished spans in the transaction will also be sent to Sentry. + +# :param scope: The Scope to use for this transaction. +# If not provided, the current Scope will be used. +# :param end_timestamp: Optional timestamp that should +# be used as timestamp instead of the current time. +# :param hub: The hub to use for this transaction. +# This argument is DEPRECATED. Please use the `scope` +# parameter, instead. + +# :return: The event ID if the transaction was sent to Sentry, +# otherwise None. +# """ +# if self.timestamp is not None: +# # This transaction is already finished, ignore. +# return None + +# # For backwards compatibility, we must handle the case where `scope` +# # or `hub` could both either be a `Scope` or a `Hub`. +# scope = self._get_scope_from_finish_args( +# scope, hub +# ) # type: Optional[sentry_sdk.Scope] + +# scope = scope or self.scope or sentry_sdk.get_current_scope() +# client = sentry_sdk.get_client() + +# if not client.is_active(): +# # We have no active client and therefore nowhere to send this transaction. +# return None + +# if self._span_recorder is None: +# # Explicit check against False needed because self.sampled might be None +# if self.sampled is False: +# logger.debug("Discarding transaction because sampled = False") +# else: +# logger.debug( +# "Discarding transaction because it was not started with sentry_sdk.start_transaction" +# ) + +# # This is not entirely accurate because discards here are not +# # exclusively based on sample rate but also traces sampler, but +# # we handle this the same here. +# if client.transport and has_tracing_enabled(client.options): +# if client.monitor and client.monitor.downsample_factor > 0: +# reason = "backpressure" +# else: +# reason = "sample_rate" + +# client.transport.record_lost_event(reason, data_category="transaction") + +# # Only one span (the transaction itself) is discarded, since we did not record any spans here. +# client.transport.record_lost_event(reason, data_category="span") +# return None + +# if not self.name: +# logger.warning( +# "Transaction has no name, falling back to ``." +# ) +# self.name = "" + +# super().finish(scope, end_timestamp) + +# if not self.sampled: +# # At this point a `sampled = None` should have already been resolved +# # to a concrete decision. +# if self.sampled is None: +# logger.warning("Discarding transaction without sampling decision.") + +# return None + +# finished_spans = [ +# span.to_json() +# for span in self._span_recorder.spans +# if span.timestamp is not None +# ] + +# # we do this to break the circular reference of transaction -> span +# # recorder -> span -> containing transaction (which is where we started) +# # before either the spans or the transaction goes out of scope and has +# # to be garbage collected +# self._span_recorder = None + +# contexts = {} +# contexts.update(self._contexts) +# contexts.update({"trace": self.get_trace_context()}) +# profile_context = self.get_profile_context() +# if profile_context is not None: +# contexts.update({"profile": profile_context}) + +# event = { +# "type": "transaction", +# "transaction": self.name, +# "transaction_info": {"source": self.source}, +# "contexts": contexts, +# "tags": self._tags, +# "timestamp": self.timestamp, +# "start_timestamp": self.start_timestamp, +# "spans": finished_spans, +# } # type: Event + +# if self._profile is not None and self._profile.valid(): +# event["profile"] = self._profile +# self._profile = None + +# event["measurements"] = self._measurements + +# # This is here since `to_json` is not invoked. This really should +# # be gone when we switch to onlyspans. +# if self._local_aggregator is not None: +# metrics_summary = self._local_aggregator.to_json() +# if metrics_summary: +# event["_metrics_summary"] = metrics_summary + +# return scope.capture_event(event) + +# def set_measurement(self, name, value, unit=""): +# # type: (str, float, MeasurementUnit) -> None +# self._measurements[name] = {"value": value, "unit": unit} + +# def set_context(self, key, value): +# # type: (str, Any) -> None +# """Sets a context. Transactions can have multiple contexts +# and they should follow the format described in the "Contexts Interface" +# documentation. + +# :param key: The name of the context. +# :param value: The information about the context. +# """ +# self._contexts[key] = value + +# def set_http_status(self, http_status): +# # type: (int) -> None +# """Sets the status of the Transaction according to the given HTTP status. + +# :param http_status: The HTTP status code.""" +# super().set_http_status(http_status) +# self.set_context("response", {"status_code": http_status}) + +# def to_json(self): +# # type: () -> Dict[str, Any] +# """Returns a JSON-compatible representation of the transaction.""" +# rv = super().to_json() + +# rv["name"] = self.name +# rv["source"] = self.source +# rv["sampled"] = self.sampled + +# return rv + +# def get_baggage(self): +# # type: () -> Baggage +# """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` +# associated with the Transaction. + +# The first time a new baggage with Sentry items is made, +# it will be frozen.""" + +# if not self._baggage or self._baggage.mutable: +# self._baggage = Baggage.populate_from_transaction(self) + +# return self._baggage + +# def _set_initial_sampling_decision(self, sampling_context): +# # type: (SamplingContext) -> None +# """ +# Sets the transaction's sampling decision, according to the following +# precedence rules: + +# 1. If a sampling decision is passed to `start_transaction` +# (`start_transaction(name: "my transaction", sampled: True)`), that +# decision will be used, regardless of anything else + +# 2. If `traces_sampler` is defined, its decision will be used. It can +# choose to keep or ignore any parent sampling decision, or use the +# sampling context data to make its own decision or to choose a sample +# rate for the transaction. + +# 3. If `traces_sampler` is not defined, but there's a parent sampling +# decision, the parent sampling decision will be used. + +# 4. If `traces_sampler` is not defined and there's no parent sampling +# decision, `traces_sample_rate` will be used. +# """ +# client = sentry_sdk.get_client() + +# transaction_description = "{op}transaction <{name}>".format( +# op=("<" + self.op + "> " if self.op else ""), name=self.name +# ) + +# # nothing to do if tracing is disabled +# if not has_tracing_enabled(client.options): +# self.sampled = False +# return + +# # if the user has forced a sampling decision by passing a `sampled` +# # value when starting the transaction, go with that +# if self.sampled is not None: +# self.sample_rate = float(self.sampled) +# return + +# # we would have bailed already if neither `traces_sampler` nor +# # `traces_sample_rate` were defined, so one of these should work; prefer +# # the hook if so +# sample_rate = ( +# client.options["traces_sampler"](sampling_context) +# if callable(client.options.get("traces_sampler")) +# else ( +# # default inheritance behavior +# sampling_context["parent_sampled"] +# if sampling_context["parent_sampled"] is not None +# else client.options["traces_sample_rate"] +# ) +# ) + +# # Since this is coming from the user (or from a function provided by the +# # user), who knows what we might get. (The only valid values are +# # booleans or numbers between 0 and 1.) +# if not is_valid_sample_rate(sample_rate, source="Tracing"): +# logger.warning( +# "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( +# transaction_description=transaction_description, +# ) +# ) +# self.sampled = False +# return + +# self.sample_rate = float(sample_rate) + +# if client.monitor: +# self.sample_rate /= 2**client.monitor.downsample_factor + +# # if the function returned 0 (or false), or if `traces_sample_rate` is +# # 0, it's a sign the transaction should be dropped +# if not self.sample_rate: +# logger.debug( +# "[Tracing] Discarding {transaction_description} because {reason}".format( +# transaction_description=transaction_description, +# reason=( +# "traces_sampler returned 0 or False" +# if callable(client.options.get("traces_sampler")) +# else "traces_sample_rate is set to 0" +# ), +# ) +# ) +# self.sampled = False +# return + +# # Now we roll the dice. random.random is inclusive of 0, but not of 1, +# # so strict < is safe here. In case sample_rate is a boolean, cast it +# # to a float (True becomes 1.0 and False becomes 0.0) +# self.sampled = random.random() < self.sample_rate + +# if self.sampled: +# logger.debug( +# "[Tracing] Starting {transaction_description}".format( +# transaction_description=transaction_description, +# ) +# ) +# else: +# logger.debug( +# "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( +# transaction_description=transaction_description, +# sample_rate=self.sample_rate, +# ) +# ) + + +# class NoOpSpan(Span): +# def __repr__(self): +# # type: () -> str +# return "<%s>" % self.__class__.__name__ + +# @property +# def containing_transaction(self): +# # type: () -> Optional[Transaction] +# return None + +# def start_child(self, **kwargs): +# # type: (**Any) -> NoOpSpan +# return NoOpSpan() + +# def to_traceparent(self): +# # type: () -> str +# return "" + +# def to_baggage(self): +# # type: () -> Optional[Baggage] +# return None + +# def get_baggage(self): +# # type: () -> Optional[Baggage] +# return None + +# def iter_headers(self): +# # type: () -> Iterator[Tuple[str, str]] +# return iter(()) + +# def set_tag(self, key, value): +# # type: (str, Any) -> None +# pass + +# def set_data(self, key, value): +# # type: (str, Any) -> None +# pass + +# def set_status(self, value): +# # type: (str) -> None +# pass + +# def set_http_status(self, http_status): +# # type: (int) -> None +# pass + +# def is_success(self): +# # type: () -> bool +# return True + +# def to_json(self): +# # type: () -> Dict[str, Any] +# return {} + +# def get_trace_context(self): +# # type: () -> Any +# return {} + +# def get_profile_context(self): +# # type: () -> Any +# return {} + +# def finish( +# self, +# scope=None, # type: Optional[sentry_sdk.Scope] +# end_timestamp=None, # type: Optional[Union[float, datetime]] +# *, +# hub=None, # type: Optional[sentry_sdk.Hub] +# ): +# # type: (...) -> Optional[str] +# """ +# The `hub` parameter is deprecated. Please use the `scope` parameter, instead. +# """ +# pass + +# def set_measurement(self, name, value, unit=""): +# # type: (str, float, MeasurementUnit) -> None +# pass + +# def set_context(self, key, value): +# # type: (str, Any) -> None +# pass + +# def init_span_recorder(self, maxlen): +# # type: (int) -> None +# pass + +# def _set_initial_sampling_decision(self, sampling_context): +# # type: (SamplingContext) -> None +# pass - child = Span( - trace_id=self.trace_id, - parent_span_id=self.span_id, - containing_transaction=self.containing_transaction, - **kwargs, - ) - - span_recorder = ( - self.containing_transaction and self.containing_transaction._span_recorder - ) - if span_recorder: - span_recorder.add(child) - - return child - - @classmethod - def continue_from_environ( - cls, - environ, # type: Mapping[str, str] - **kwargs, # type: Any - ): - # type: (...) -> Transaction - """ - Create a Transaction with the given params, then add in data pulled from - the ``sentry-trace`` and ``baggage`` headers from the environ (if any) - before returning the Transaction. - - This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` - in that it assumes header names in the form ``HTTP_HEADER_NAME`` - - such as you would get from a WSGI/ASGI environ - - rather than the form ``header-name``. - - :param environ: The ASGI/WSGI environ to pull information from. - """ - if cls is Span: - logger.warning( - "Deprecated: use Transaction.continue_from_environ " - "instead of Span.continue_from_environ." - ) - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers, # type: Mapping[str, str] - **kwargs, # type: Any - ): - # type: (...) -> Transaction - """ - Create a transaction with the given params (including any data pulled from - the ``sentry-trace`` and ``baggage`` headers). - - :param headers: The dictionary with the HTTP headers to pull information from. - """ - # TODO move this to the Transaction class - if cls is Span: - logger.warning( - "Deprecated: use Transaction.continue_from_headers " - "instead of Span.continue_from_headers." - ) - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header(headers.get(BAGGAGE_HEADER_NAME)) - kwargs.update({BAGGAGE_HEADER_NAME: baggage}) - - sentrytrace_kwargs = extract_sentrytrace_data( - headers.get(SENTRY_TRACE_HEADER_NAME) - ) - - if sentrytrace_kwargs is not None: - kwargs.update(sentrytrace_kwargs) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self): - # type: () -> Iterator[Tuple[str, str]] - """ - Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. - If the span's containing transaction doesn't yet have a ``baggage`` value, - this will cause one to be generated and stored. - """ - if not self.containing_transaction: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - return - - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - baggage = self.containing_transaction.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - @classmethod - def from_traceparent( - cls, - traceparent, # type: Optional[str] - **kwargs, # type: Any - ): - # type: (...) -> Optional[Transaction] - """ - DEPRECATED: Use :py:meth:`sentry_sdk.tracing.Span.continue_from_headers`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - logger.warning( - "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) " - "instead of from_traceparent(traceparent, **kwargs)" - ) - - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self): - # type: () -> str - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def to_baggage(self): - # type: () -> Optional[Baggage] - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with this ``Span``, if any. (Taken from the root of the span tree.) - """ - if self.containing_transaction: - return self.containing_transaction.get_baggage() - return None - - def set_tag(self, key, value): - # type: (str, Any) -> None - self._tags[key] = value - - def set_data(self, key, value): - # type: (str, Any) -> None - self._data[key] = value - - def set_status(self, value): - # type: (str) -> None - self.status = value - - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - self._measurements[name] = {"value": value, "unit": unit} - - def set_thread(self, thread_id, thread_name): - # type: (Optional[int], Optional[str]) -> None - - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) - - def set_profiler_id(self, profiler_id): - # type: (Optional[str]) -> None - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status): - # type: (int) -> None - self.set_tag( - "http.status_code", str(http_status) - ) # we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self): - # type: () -> bool - return self.status == "ok" - - def finish(self, scope=None, end_timestamp=None): - # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ - if self.timestamp is not None: - # This span is already finished, ignore. - return None - - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp - else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - except AttributeError: - self.timestamp = datetime.now(timezone.utc) - - scope = scope or sentry_sdk.get_current_scope() - maybe_create_breadcrumbs_from_span(scope, self) - - return None - - def to_json(self): - # type: () -> Dict[str, Any] - """Returns a JSON-compatible representation of the span.""" - - rv = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "same_process_as_parent": self.same_process_as_parent, - "op": self.op, - "description": self.description, - "start_timestamp": self.start_timestamp, - "timestamp": self.timestamp, - "origin": self.origin, - } # type: Dict[str, Any] - - if self.status: - self._tags["status"] = self.status - - if self._local_aggregator is not None: - metrics_summary = self._local_aggregator.to_json() - if metrics_summary: - rv["_metrics_summary"] = metrics_summary - - if len(self._measurements) > 0: - rv["measurements"] = self._measurements - - tags = self._tags - if tags: - rv["tags"] = tags - - data = self._data - if data: - rv["data"] = data - - return rv - - def get_trace_context(self): - # type: () -> Any - rv = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "op": self.op, - "description": self.description, - "origin": self.origin, - } # type: Dict[str, Any] - if self.status: - rv["status"] = self.status - - if self.containing_transaction: - rv["dynamic_sampling_context"] = ( - self.containing_transaction.get_baggage().dynamic_sampling_context() - ) - - data = {} - - thread_id = self._data.get(SPANDATA.THREAD_ID) - if thread_id is not None: - data["thread.id"] = thread_id - - thread_name = self._data.get(SPANDATA.THREAD_NAME) - if thread_name is not None: - data["thread.name"] = thread_name - - if data: - rv["data"] = data - - return rv - - def get_profile_context(self): - # type: () -> Optional[ProfileContext] - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - -class Transaction(Span): - """The Transaction is the root element that holds all the spans - for Sentry performance instrumentation. - - :param name: Identifier of the transaction. - Will show up in the Sentry UI. - :param parent_sampled: Whether the parent transaction was sampled. - If True this transaction will be kept, if False it will be discarded. - :param baggage: The W3C baggage header value. - (see https://www.w3.org/TR/baggage/) - :param source: A string describing the source of the transaction name. - This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations - for more information. Default "custom". - :param kwargs: Additional arguments to be passed to the Span constructor. - See :py:class:`sentry_sdk.tracing.Span` for available arguments. - """ - - __slots__ = ( - "name", - "source", - "parent_sampled", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_measurements", - "_contexts", - "_profile", - "_baggage", - ) - - def __init__( - self, - name="", # type: str - parent_sampled=None, # type: Optional[bool] - baggage=None, # type: Optional[Baggage] - source=TRANSACTION_SOURCE_CUSTOM, # type: str - **kwargs, # type: Unpack[SpanKwargs] - ): - # type: (...) -> None - - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate = None # type: Optional[float] - self.parent_sampled = parent_sampled - self._measurements = {} # type: Dict[str, MeasurementValue] - self._contexts = {} # type: Dict[str, Any] - self._profile = ( - None - ) # type: Optional[sentry_sdk.profiler.transaction_profiler.Profile] - self._baggage = baggage - - def __repr__(self): - # type: () -> str - return ( - "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.name, - self.op, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.source, - self.origin, - ) - ) - - def _possibly_started(self): - # type: () -> bool - """Returns whether the transaction might have been started. - - If this returns False, we know that the transaction was not started - with sentry_sdk.start_transaction, and therefore the transaction will - be discarded. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self): - # type: () -> Transaction - if not self._possibly_started(): - logger.debug( - "Transaction was entered without being started with sentry_sdk.start_transaction." - "The transaction will not be sent to Sentry. To fix, start the transaction by" - "passing it to sentry_sdk.start_transaction." - ) - - super().__enter__() - - if self._profile is not None: - self._profile.__enter__() - - return self - - def __exit__(self, ty, value, tb): - # type: (Optional[Any], Optional[Any], Optional[Any]) -> None - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self): - # type: () -> Transaction - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def _get_scope_from_finish_args( - self, - scope_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] - hub_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] - ): - # type: (...) -> Optional[sentry_sdk.Scope] - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - - def finish( - self, - scope=None, # type: Optional[sentry_sdk.Scope] - end_timestamp=None, # type: Optional[Union[float, datetime]] - *, - hub=None, # type: Optional[sentry_sdk.Hub] - ): - # type: (...) -> Optional[str] - """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. - - :param scope: The Scope to use for this transaction. - If not provided, the current Scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args( - scope, hub - ) # type: Optional[sentry_sdk.Scope] - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be None - if self.sampled is False: - logger.debug("Discarding transaction because sampled = False") - else: - logger.debug( - "Discarding transaction because it was not started with sentry_sdk.start_transaction" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - if client.transport and has_tracing_enabled(client.options): - if client.monitor and client.monitor.downsample_factor > 0: - reason = "backpressure" - else: - reason = "sample_rate" - - client.transport.record_lost_event(reason, data_category="transaction") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - if self.sampled is None: - logger.warning("Discarding transaction without sampling decision.") - - return None - - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - self._span_recorder = None - - contexts = {} - contexts.update(self._contexts) - contexts.update({"trace": self.get_trace_context()}) - profile_context = self.get_profile_context() - if profile_context is not None: - contexts.update({"profile": profile_context}) - - event = { - "type": "transaction", - "transaction": self.name, - "transaction_info": {"source": self.source}, - "contexts": contexts, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": finished_spans, - } # type: Event - - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - - event["measurements"] = self._measurements - - # This is here since `to_json` is not invoked. This really should - # be gone when we switch to onlyspans. - if self._local_aggregator is not None: - metrics_summary = self._local_aggregator.to_json() - if metrics_summary: - event["_metrics_summary"] = metrics_summary - - return scope.capture_event(event) - - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - self._measurements[name] = {"value": value, "unit": unit} - - def set_context(self, key, value): - # type: (str, Any) -> None - """Sets a context. Transactions can have multiple contexts - and they should follow the format described in the "Contexts Interface" - documentation. - - :param key: The name of the context. - :param value: The information about the context. - """ - self._contexts[key] = value - - def set_http_status(self, http_status): - # type: (int) -> None - """Sets the status of the Transaction according to the given HTTP status. - - :param http_status: The HTTP status code.""" - super().set_http_status(http_status) - self.set_context("response", {"status_code": http_status}) - - def to_json(self): - # type: () -> Dict[str, Any] - """Returns a JSON-compatible representation of the transaction.""" - rv = super().to_json() - - rv["name"] = self.name - rv["source"] = self.source - rv["sampled"] = self.sampled - - return rv - - def get_trace_context(self): - # type: () -> Any - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self): - # type: () -> Baggage - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with the Transaction. - - The first time a new baggage with Sentry items is made, - it will be frozen.""" - - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_transaction(self) - - return self._baggage - - def _set_initial_sampling_decision(self, sampling_context): - # type: (SamplingContext) -> None - """ - Sets the transaction's sampling decision, according to the following - precedence rules: - - 1. If a sampling decision is passed to `start_transaction` - (`start_transaction(name: "my transaction", sampled: True)`), that - decision will be used, regardless of anything else - - 2. If `traces_sampler` is defined, its decision will be used. It can - choose to keep or ignore any parent sampling decision, or use the - sampling context data to make its own decision or to choose a sample - rate for the transaction. - - 3. If `traces_sampler` is not defined, but there's a parent sampling - decision, the parent sampling decision will be used. - - 4. If `traces_sampler` is not defined and there's no parent sampling - decision, `traces_sample_rate` will be used. - """ - client = sentry_sdk.get_client() - - transaction_description = "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - sample_rate = ( - client.options["traces_sampler"](sampling_context) - if callable(client.options.get("traces_sampler")) - else ( - # default inheritance behavior - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning( - "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( - transaction_description=transaction_description, - ) - ) - self.sampled = False - return - - self.sample_rate = float(sample_rate) - - if client.monitor: - self.sample_rate /= 2**client.monitor.downsample_factor - - # if the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - if not self.sample_rate: - logger.debug( - "[Tracing] Discarding {transaction_description} because {reason}".format( - transaction_description=transaction_description, - reason=( - "traces_sampler returned 0 or False" - if callable(client.options.get("traces_sampler")) - else "traces_sample_rate is set to 0" - ), - ) - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < self.sample_rate - - if self.sampled: - logger.debug( - "[Tracing] Starting {transaction_description}".format( - transaction_description=transaction_description, - ) - ) - else: - logger.debug( - "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( - transaction_description=transaction_description, - sample_rate=self.sample_rate, - ) - ) - - -class NoOpSpan(Span): - def __repr__(self): - # type: () -> str - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self): - # type: () -> Optional[Transaction] - return None - - def start_child(self, **kwargs): - # type: (**Any) -> NoOpSpan - return NoOpSpan() - - def to_traceparent(self): - # type: () -> str - return "" - - def to_baggage(self): - # type: () -> Optional[Baggage] - return None - - def get_baggage(self): - # type: () -> Optional[Baggage] - return None - - def iter_headers(self): - # type: () -> Iterator[Tuple[str, str]] - return iter(()) - - def set_tag(self, key, value): - # type: (str, Any) -> None - pass - - def set_data(self, key, value): - # type: (str, Any) -> None - pass - - def set_status(self, value): - # type: (str) -> None - pass - - def set_http_status(self, http_status): - # type: (int) -> None - pass - - def is_success(self): - # type: () -> bool - return True - - def to_json(self): - # type: () -> Dict[str, Any] - return {} - - def get_trace_context(self): - # type: () -> Any - return {} - - def get_profile_context(self): - # type: () -> Any - return {} - - def finish( - self, - scope=None, # type: Optional[sentry_sdk.Scope] - end_timestamp=None, # type: Optional[Union[float, datetime]] - *, - hub=None, # type: Optional[sentry_sdk.Hub] - ): - # type: (...) -> Optional[str] - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ - pass - - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - pass - - def set_context(self, key, value): - # type: (str, Any) -> None - pass - - def init_span_recorder(self, maxlen): - # type: (int) -> None - pass - - def _set_initial_sampling_decision(self, sampling_context): - # type: (SamplingContext) -> None - pass - - -class POTelSpan: +class Span: """ OTel span wrapper providing compatibility with the old span interface. - """ - - # XXX Maybe it makes sense to repurpose the existing Span class for this. - # For now I'm keeping this class separate to have a clean slate. - # XXX The wrapper itself should have as little state as possible + The wrapper itself should have as little state as possible. Everything + persistent should be stored on the underlying OTel span. + """ def __init__( self, @@ -1284,6 +1273,8 @@ def __init__( description or op or "", start_time=start_timestamp ) # XXX + # XXX deal with _otel_span being a NonRecordingSpan + self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, origin) if op is not None: @@ -1297,7 +1288,7 @@ def __init__( self.scope = scope def __enter__(self): - # type: () -> POTelSpan + # type: () -> Span # XXX use_span? https://github.com/open-telemetry/opentelemetry-python/blob/3836da8543ce9751051e38a110c0468724042e62/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547 # # create a Context object with parent set as current span @@ -1306,7 +1297,6 @@ def __enter__(self): self._ctx_token = context.attach(ctx) scope = self.scope or sentry_sdk.get_current_scope() scope.span = self - return self def __exit__(self, ty, value, tb): @@ -1315,6 +1305,22 @@ def __exit__(self, ty, value, tb): # XXX set status to error if unset and an exception occurred? context.detach(self._ctx_token) + @property + def name(self): + pass + + @name.setter + def name(self, value): + pass + + @property + def source(self): + pass + + @source.setter + def source(self, value): + pass + @property def description(self): # type: () -> Optional[str] @@ -1347,6 +1353,9 @@ def origin(self, value): @property def root_span(self): + if isinstance(self._otel_span, otel_trace.NonRecordingSpan): + return None + parent = None while True: # XXX @@ -1357,9 +1366,16 @@ def root_span(self): return parent + @property + def is_root_span(self): + if isinstance(self._otel_span, otel_trace.NonRecordingSpan): + return False + + return self._otel_span.parent is None + @property def containing_transaction(self): - # type: () -> Optional[Transaction] + # type: () -> Optional[Span] """ Get the transaction this span is a child of. @@ -1403,9 +1419,9 @@ def op(self, value): self._otel_span.set_attribute(SentrySpanAttribute.OP, value) def start_child(self, **kwargs): - # type: (str, **Any) -> POTelSpan + # type: (str, **Any) -> Span kwargs.setdefault("sampled", self.sampled) - child_span = POTelSpan(**kwargs) + child_span = Span(**kwargs) return child_span @classmethod @@ -1414,9 +1430,9 @@ def continue_from_environ( environ, # type: Mapping[str, str] **kwargs, # type: Any ): - # type: (...) -> POTelSpan + # type: (...) -> Span # XXX actually propagate - span = POTelSpan(**kwargs) + span = Span(**kwargs) return span @classmethod @@ -1425,9 +1441,9 @@ def continue_from_headers( headers, # type: Mapping[str, str] **kwargs, # type: Any ): - # type: (...) -> POTelSpan + # type: (...) -> Span # XXX actually propagate - span = POTelSpan(**kwargs) + span = Span(**kwargs) return span def iter_headers(self): @@ -1440,7 +1456,7 @@ def from_traceparent( traceparent, # type: Optional[str] **kwargs, # type: Any ): - # type: (...) -> Optional[Transaction] + # type: (...) -> Optional[Span] pass def to_traceparent(self): @@ -1546,6 +1562,14 @@ def get_baggage(self): pass +Transaction = Span + + +class NoOpSpan: + # XXX + pass + + if TYPE_CHECKING: @overload diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 0dabfbc486..a39b5d61f4 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -687,7 +687,7 @@ def func_with_tracing(*args, **kwargs): def get_current_span(scope=None): - # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] + # type: (Optional[sentry_sdk.Scope]) -> Optional[sentry_sdk.tracing.Span] """ Returns the currently active span if there is one running, otherwise `None` """ @@ -702,6 +702,3 @@ def get_current_span(scope=None): LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/tests/new_scopes_compat/__init__.py b/tests/new_scopes_compat/__init__.py deleted file mode 100644 index 45391bd9ad..0000000000 --- a/tests/new_scopes_compat/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Separate module for tests that check backwards compatibility of the Hub API with 1.x. -These tests should be removed once we remove the Hub API, likely in the next major. - -All tests in this module are run with hub isolation, provided by `isolate_hub` autouse -fixture, defined in `conftest.py`. -""" diff --git a/tests/new_scopes_compat/conftest.py b/tests/new_scopes_compat/conftest.py deleted file mode 100644 index 9f16898dea..0000000000 --- a/tests/new_scopes_compat/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest -import sentry_sdk - - -@pytest.fixture(autouse=True) -def isolate_hub(suppress_deprecation_warnings): - with sentry_sdk.Hub(None): - yield diff --git a/tests/new_scopes_compat/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py deleted file mode 100644 index f1e5ef3dfa..0000000000 --- a/tests/new_scopes_compat/test_new_scopes_compat.py +++ /dev/null @@ -1,224 +0,0 @@ -import pytest - -import sentry_sdk -from sentry_sdk.hub import Hub - -""" -Those tests are meant to check the compatibility of the new scopes in SDK 2.0 with the old Hub/Scope system in SDK 1.x. - -Those tests have been run with the latest SDK 1.x versiona and the data used in the `assert` statements represents -the behvaior of the SDK 1.x. - -This makes sure that we are backwards compatible. (on a best effort basis, there will probably be some edge cases that are not covered here) -""" - - -def test_with_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} - - -@pytest.mark.xfail(reason="will be removed in 3.0") -def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == { - "A": 1, - "B1": 1, - "B2": 1, - "B3": 1, - "B4": 1, - "B5": 1, - "Z": 1, - } - - -@pytest.mark.xfail(reason="will be removed in 3.0") -def test_with_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B5": 1, "Z": 1} - - -@pytest.mark.xfail(reason="will be removed in 3.0") -def test_with_cloned_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -@pytest.mark.xfail(reason="will be removed in 3.0") -def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -@pytest.mark.xfail(reason="will be removed in 3.0") -def test_with_cloned_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} From 2312a12ed7527c376e5440d95720bc90f31318d4 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Aug 2024 14:20:15 +0200 Subject: [PATCH 14/16] more big wip --- sentry_sdk/api.py | 12 +- .../opentelemetry/potel_span_processor.py | 18 +- .../integrations/opentelemetry/utils.py | 11 +- sentry_sdk/integrations/wsgi.py | 6 +- sentry_sdk/scope.py | 116 +++---- sentry_sdk/tracing.py | 296 +++++++----------- 6 files changed, 196 insertions(+), 263 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 33f443b120..13d4dedca5 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -232,7 +232,8 @@ def flush( def start_span( - root_span=None, + *, + segment_span=None, custom_sampling_context=None, **kwargs, # type: Any ): @@ -241,10 +242,13 @@ def start_span( Start and return a span. """ # TODO: Consider adding type hints to the method signature. - return get_current_scope().start_span(root_span, custom_sampling_context, **kwargs) + return get_current_scope().start_span( + segment_span, custom_sampling_context, **kwargs + ) def start_transaction( + *, transaction=None, # type: Optional[Span] custom_sampling_context=None, # type: Optional[SamplingContext] **kwargs, # type: Unpack[TransactionKwargs] @@ -284,7 +288,9 @@ def start_transaction( available arguments. """ return get_current_scope().start_span( - root_span=transaction, custom_sampling_context=custom_sampling_context, **kwargs + segment_span=transaction, + custom_sampling_context=custom_sampling_context, + **kwargs, ) diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py index cddaf24ab2..e9f76ba817 100644 --- a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -7,7 +7,7 @@ from sentry_sdk import capture_event from sentry_sdk.integrations.opentelemetry.utils import ( is_sentry_span, - convert_otel_timestamp, + convert_from_otel_timestamp, extract_span_data, ) from sentry_sdk.integrations.opentelemetry.consts import ( @@ -53,7 +53,7 @@ def on_end(self, span): self._children_spans[span.parent.span_id].append(span) else: # if have a root span ending, we build a transaction and send it - self._flush_root_span(span) + self._flush_segment_span(span) # TODO-neel-potel not sure we need a clear like JS def shutdown(self): @@ -66,9 +66,9 @@ def force_flush(self, timeout_millis=30000): # type: (int) -> bool return True - def _flush_root_span(self, span): + def _flush_segment_span(self, span): # type: (ReadableSpan) -> None - transaction_event = self._root_span_to_transaction_event(span) + transaction_event = self._segment_span_to_transaction_event(span) if not transaction_event: return @@ -103,7 +103,7 @@ def _collect_children(self, span): # we construct the event from scratch here # and not use the current Transaction class for easier refactoring - def _root_span_to_transaction_event(self, span): + def _segment_span_to_transaction_event(self, span): # type: (ReadableSpan) -> Optional[Event] if not span.context: return None @@ -141,8 +141,8 @@ def _root_span_to_transaction_event(self, span): # TODO-neel-potel tx source based on integration "transaction_info": {"source": "custom"}, "contexts": contexts, - "start_timestamp": convert_otel_timestamp(span.start_time), - "timestamp": convert_otel_timestamp(span.end_time), + "start_timestamp": convert_from_otel_timestamp(span.start_time), + "timestamp": convert_from_otel_timestamp(span.end_time), } # type: Event return event @@ -168,8 +168,8 @@ def _span_to_json(self, span): "op": op, "description": description, "status": status, - "start_timestamp": convert_otel_timestamp(span.start_time), - "timestamp": convert_otel_timestamp(span.end_time), + "start_timestamp": convert_from_otel_timestamp(span.start_time), + "timestamp": convert_from_otel_timestamp(span.end_time), "origin": origin or SPAN_ORIGIN, } # type: dict[str, Any] diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index ecb1852404..49b0931dca 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -72,11 +72,20 @@ def is_sentry_span(span): return False -def convert_otel_timestamp(time): +def convert_from_otel_timestamp(time): # type: (int) -> datetime + """Convert an OTel ns-level timestamp to a datetime.""" return datetime.fromtimestamp(time / 1e9, timezone.utc) +def convert_to_otel_timestamp(time): + # type: (Union[datetime.datetime, float]) -> int + """Convert a datetime to an OTel timestamp (with ns precision).""" + if isinstance(time, datetime): + return int(time.timestamp() * 1e9) + return int(time * 1e9) + + def extract_span_data(span): # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] op = span.name diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 4ac801ea88..2d8308dc98 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -91,7 +91,7 @@ def __call__(self, environ, start_response): ) ) - root_span = continue_trace( + segment_span = continue_trace( environ, op=OP.HTTP_SERVER, name="generic WSGI request", @@ -100,13 +100,13 @@ def __call__(self, environ, start_response): ) with sentry_sdk.start_transaction( - root_span, custom_sampling_context={"wsgi_environ": environ} + segment_span, custom_sampling_context={"wsgi_environ": environ} ): try: response = self.app( environ, partial( - _sentry_start_response, start_response, root_span + _sentry_start_response, start_response, segment_span ), ) except BaseException: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index f146196082..8fcd972c53 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -166,8 +166,8 @@ class Scope(object): "_level", "_name", "_fingerprint", - "_root_span_name", - "_root_span_info", + "_segment_span_name", + "_segment_span_info", "_user", "_tags", "_contexts", @@ -219,8 +219,8 @@ def __copy__(self): rv._level = self._level rv._name = self._name rv._fingerprint = self._fingerprint - rv._root_span_name = self._root_span_name - rv._root_span_info = dict(self._root_span_info) + rv._segment_span_name = self._segment_span_name + rv._segment_span_info = dict(self._segment_span_info) rv._user = self._user rv._tags = dict(self._tags) @@ -670,8 +670,8 @@ def clear(self): """Clears the entire scope.""" self._level = None # type: Optional[LogLevelStr] self._fingerprint = None # type: Optional[List[str]] - self._root_span_name = None # type: Optional[str] - self._root_span_info = {} # type: MutableMapping[str, str] + self._segment_span_name = None # type: Optional[str] + self._segment_span_info = {} # type: MutableMapping[str, str] self._user = None # type: Optional[Dict[str, Any]] self._tags = {} # type: Dict[str, Any] @@ -716,29 +716,29 @@ def transaction(self): Return the transaction in the scope, if any. .. deprecated:: 3.0.0 - This function is deprecated and will be removed in a future release. Use Scope.root_span instead. + This function is deprecated and will be removed in a future release. Use Scope.segment_span instead. """ logger.warning( - "Deprecated: use Scope.root_span instead. This will be removed in the future." + "Deprecated: use Scope.segment_span instead. This will be removed in the future." ) - return self.root_span + return self.segment_span @property - def root_span(self): + def segment_span(self): """Return the root span in the scope, if any.""" # there is no span/transaction on the scope if self._span is None: return None # there is an orphan span on the scope - if self._span.root_span is None: + if self._span.segment_span is None: return None # there is either a root span (which is its own containing # root span) or a non-orphan span on the scope - return self._span.root_span + return self._span.segment_span def set_transaction_name(self, name, source=None): # type: (str, Optional[str]) -> None @@ -746,19 +746,19 @@ def set_transaction_name(self, name, source=None): Set the transaction name and optionally the transaction source. .. deprecated:: 3.0.0 - This function is deprecated and will be removed in a future release. Use Scope.set_root_span_name instead. + This function is deprecated and will be removed in a future release. Use Scope.set_segment_span_name instead. """ - self.set_root_span_name(name, source) + self.set_segment_span_name(name, source) - def set_root_span_name(self, name, source=None): + def set_segment_span_name(self, name, source=None): """Set the root span name and optionally the source.""" - self._root_span_name = name - if self._span and self._span.root_span: - self._span.root_span.name = name + self._segment_span_name = name + if self._span and self._span.segment_span: + self._span.segment_span.name = name if source: - self._span.root_span.source = source + self._span.segment_span.source = source if source: - self._root_span_info["source"] = source + self._segment_span_info["source"] = source @_attr_setter def user(self, value): @@ -788,11 +788,11 @@ def span(self, span): # XXX: this differs from the implementation in JS, there Scope.setSpan # does not set root span name. return - if span.is_root_span: + if span.is_segment: if span.name: - self._root_span_name = span.name + self._segment_span_name = span.name if span.source: - self._root_span_info["source"] = span.source + self._segment_span_info["source"] = span.source @property def profile(self): @@ -987,37 +987,20 @@ def start_transaction( constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ - kwargs.setdefault("scope", self) - span = Span(**kwargs) - return span - - # XXX - kwargs.setdefault("scope", self) - - client = self.get_client() - try_autostart_continuous_profiler() - custom_sampling_context = custom_sampling_context or {} - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs = kwargs # type: TransactionKwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) + kwargs.setdefault("scope", self) + span = transaction or Span(**kwargs) - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision sampling_context = { "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) + span._set_initial_sampling_decision(sampling_context=sampling_context) - if transaction.sampled: + if span.sampled: profile = Profile( transaction.sampled, transaction._start_timestamp_monotonic_ns ) @@ -1027,12 +1010,14 @@ def start_transaction( # we don't bother to keep spans if we already know we're not going to # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) + max_spans = ( + self.get_client().options["_experiments"].get("max_spans") + ) or 1000 + span.init_span_recorder(maxlen=max_spans) - return transaction + return span - def start_span(self, root_span=None, custom_sampling_context=None, **kwargs): + def start_span(self, segment_span=None, custom_sampling_context=None, **kwargs): # type: (Optional[Span], Optional[SamplingContext], Any) -> Span """ Start a span whose parent is the currently active span or transaction, if any. @@ -1044,8 +1029,8 @@ def start_span(self, root_span=None, custom_sampling_context=None, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ kwargs.setdefault("scope", self) - if root_span: - return root_span + if segment_span: + return segment_span span = self.span or self.get_isolation_scope().span @@ -1089,7 +1074,7 @@ def continue_trace( """ self.generate_propagation_context(environ_or_headers) - root_span = Span.continue_from_headers( + segment_span = Span.continue_from_headers( normalize_incoming_data(environ_or_headers), op=op, origin=origin, @@ -1097,7 +1082,7 @@ def continue_trace( source=source, ) - return root_span + return segment_span def capture_event(self, event, hint=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str] @@ -1301,15 +1286,18 @@ def _apply_user_to_event(self, event, hint, options): if event.get("user") is None and self._user is not None: event["user"] = self._user - def _apply_root_span_name_to_event(self, event, hint, options): + def _apply_segment_span_name_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction") is None and self._root_span_name is not None: - event["transaction"] = self._root_span_name + if event.get("transaction") is None and self._segment_span_name is not None: + event["transaction"] = self._segment_span_name - def _apply_root_span_info_to_event(self, event, hint, options): + def _apply_segment_span_info_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction_info") is None and self._root_span_info is not None: - event["transaction_info"] = self._root_span_info + if ( + event.get("transaction_info") is None + and self._segment_span_info is not None + ): + event["transaction_info"] = self._segment_span_info def _apply_fingerprint_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None @@ -1431,8 +1419,8 @@ def apply_to_event( self._apply_level_to_event(event, hint, options) self._apply_fingerprint_to_event(event, hint, options) self._apply_user_to_event(event, hint, options) - self._apply_root_span_name_to_event(event, hint, options) - self._apply_root_span_info_to_event(event, hint, options) + self._apply_segment_span_name_to_event(event, hint, options) + self._apply_segment_span_info_to_event(event, hint, options) self._apply_tags_to_event(event, hint, options) self._apply_extra_to_event(event, hint, options) @@ -1456,10 +1444,10 @@ def update_from_scope(self, scope): self._level = scope._level if scope._fingerprint is not None: self._fingerprint = scope._fingerprint - if scope._root_span_name is not None: - self._root_span_name = scope._root_span_name - if scope._root_span_info is not None: - self._root_span_info.update(scope._root_span_info) + if scope._segment_span_name is not None: + self._segment_span_name = scope._segment_span_name + if scope._segment_span_info is not None: + self._segment_span_info.update(scope._segment_span_info) if scope._user is not None: self._user = scope._user if scope._tags: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index e540b4339f..529b7958bc 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -219,31 +219,6 @@ def add(self, span): # class Span: -# """A span holds timing information of a block of code. -# Spans can have multiple child spans thus forming a span tree. - -# :param trace_id: The trace ID of the root span. If this new span is to be the root span, -# omit this parameter, and a new trace ID will be generated. -# :param span_id: The span ID of this span. If omitted, a new span ID will be generated. -# :param parent_span_id: The span ID of the parent span, if applicable. -# :param same_process_as_parent: Whether this span is in the same process as the parent span. -# :param sampled: Whether the span should be sampled. Overrides the default sampling decision -# for this span when provided. -# :param op: The span's operation. A list of recommended values is available here: -# https://develop.sentry.dev/sdk/performance/span-operations/ -# :param description: A description of what operation is being performed within the span. -# :param hub: The hub to use for this span. - -# .. deprecated:: 2.0.0 -# Please use the `scope` parameter, instead. -# :param status: The span's status. Possible values are listed at -# https://develop.sentry.dev/sdk/event-payloads/span/ -# :param containing_transaction: The transaction that this span belongs to. -# :param start_timestamp: The timestamp when the span started. If omitted, the current time -# will be used. -# :param scope: The scope to use for this span. If not provided, we use the current scope. -# """ - # __slots__ = ( # "trace_id", # "span_id", @@ -285,82 +260,6 @@ def add(self, span): # origin="manual", # type: str # ): # # type: (...) -> None -# self.trace_id = trace_id or uuid.uuid4().hex -# self.span_id = span_id or uuid.uuid4().hex[16:] -# self.parent_span_id = parent_span_id -# self.same_process_as_parent = same_process_as_parent -# self.sampled = sampled -# self.op = op -# self.description = description -# self.status = status -# self.hub = hub # backwards compatibility -# self.scope = scope -# self.origin = origin -# self._measurements = {} # type: Dict[str, MeasurementValue] -# self._tags = {} # type: MutableMapping[str, str] -# self._data = {} # type: Dict[str, Any] -# self._containing_transaction = containing_transaction - -# if hub is not None: -# warnings.warn( -# "The `hub` parameter is deprecated. Please use `scope` instead.", -# DeprecationWarning, -# stacklevel=2, -# ) - -# self.scope = self.scope or hub.scope - -# if start_timestamp is None: -# start_timestamp = datetime.now(timezone.utc) -# elif isinstance(start_timestamp, float): -# start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) -# self.start_timestamp = start_timestamp -# try: -# # profiling depends on this value and requires that -# # it is measured in nanoseconds -# self._start_timestamp_monotonic_ns = nanosecond_time() -# except AttributeError: -# pass - -# #: End timestamp of span -# self.timestamp = None # type: Optional[datetime] - -# self._span_recorder = None # type: Optional[_SpanRecorder] -# self._local_aggregator = None # type: Optional[LocalAggregator] - -# thread_id, thread_name = get_current_thread_meta() -# self.set_thread(thread_id, thread_name) -# self.set_profiler_id(get_profiler_id()) - -# # TODO this should really live on the Transaction class rather than the Span -# # class -# def init_span_recorder(self, maxlen): -# # type: (int) -> None -# if self._span_recorder is None: -# self._span_recorder = _SpanRecorder(maxlen) - -# def _get_local_aggregator(self): -# # type: (...) -> LocalAggregator -# rv = self._local_aggregator -# if rv is None: -# rv = self._local_aggregator = LocalAggregator() -# return rv - -# def __repr__(self): -# # type: () -> str -# return ( -# "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" -# % ( -# self.__class__.__name__, -# self.op, -# self.description, -# self.trace_id, -# self.span_id, -# self.parent_span_id, -# self.sampled, -# self.origin, -# ) -# ) # def __enter__(self): # # type: () -> Span @@ -380,18 +279,6 @@ def add(self, span): # self.finish(scope) # scope.span = old_span -# @property -# def containing_transaction(self): -# # type: () -> Optional[Transaction] -# """The ``Transaction`` that this span belongs to. -# The ``Transaction`` is the root of the span tree, -# so one could also think of this ``Transaction`` as the "root span".""" - -# # this is a getter rather than a regular attribute so that transactions -# # can return `self` here instead (as a way to prevent them circularly -# # referencing themselves) -# return self._containing_transaction - # def start_child(self, **kwargs): # # type: (**Any) -> Span # """ @@ -535,21 +422,6 @@ def add(self, span): # {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs # ) -# def to_traceparent(self): -# # type: () -> str -# if self.sampled is True: -# sampled = "1" -# elif self.sampled is False: -# sampled = "0" -# else: -# sampled = None - -# traceparent = "%s-%s" % (self.trace_id, self.span_id) -# if sampled is not None: -# traceparent += "-%s" % (sampled,) - -# return traceparent - # def to_baggage(self): # # type: () -> Optional[Baggage] # """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` @@ -563,44 +435,6 @@ def add(self, span): # # type: (str, Any) -> None # self._tags[key] = value -# def set_data(self, key, value): -# # type: (str, Any) -> None -# self._data[key] = value - -# def set_status(self, value): -# # type: (str) -> None -# self.status = value - -# def set_measurement(self, name, value, unit=""): -# # type: (str, float, MeasurementUnit) -> None -# self._measurements[name] = {"value": value, "unit": unit} - -# def set_thread(self, thread_id, thread_name): -# # type: (Optional[int], Optional[str]) -> None - -# if thread_id is not None: -# self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - -# if thread_name is not None: -# self.set_data(SPANDATA.THREAD_NAME, thread_name) - -# def set_profiler_id(self, profiler_id): -# # type: (Optional[str]) -> None -# if profiler_id is not None: -# self.set_data(SPANDATA.PROFILER_ID, profiler_id) - -# def set_http_status(self, http_status): -# # type: (int) -> None -# self.set_tag( -# "http.status_code", str(http_status) -# ) # we keep this for backwards compatibility -# self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) -# self.set_status(get_span_status_from_http_code(http_status)) - -# def is_success(self): -# # type: () -> bool -# return self.status == "ok" - # def finish(self, scope=None, end_timestamp=None): # # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] # """ @@ -1240,10 +1074,31 @@ def add(self, span): class Span: """ - OTel span wrapper providing compatibility with the old span interface. + A span holds timing information of a block of code. + + Spans can have multiple child spans thus forming a span tree. + + As of 3.0, this class is an OTel span wrapper providing compatibility + with the old span interface. The wrapper itself should have as little state + as possible. Everything persistent should be stored on the underlying OTel + span. - The wrapper itself should have as little state as possible. Everything - persistent should be stored on the underlying OTel span. + :param trace_id: The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + :param span_id: The span ID of this span. If omitted, a new span ID will be generated. + :param parent_span_id: The span ID of the parent span, if applicable. + :param same_process_as_parent: Whether this span is in the same process as the parent span. + :param sampled: Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + :param op: The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + :param description: A description of what operation is being performed within the span. + :param status: The span's status. Possible values are listed at + https://develop.sentry.dev/sdk/event-payloads/span/ + :param containing_transaction: The transaction that this span belongs to. + :param start_timestamp: The timestamp when the span started. If omitted, the current time + will be used. + :param scope: The scope to use for this span. If not provided, we use the current scope. """ def __init__( @@ -1256,18 +1111,35 @@ def __init__( start_timestamp=None, # type: Optional[Union[datetime, float]] origin="manual", # type: str **_, # type: dict[str, object] + # XXX old args: + # trace_id=None, # type: Optional[str] + # span_id=None, # type: Optional[str] + # parent_span_id=None, # type: Optional[str] + # same_process_as_parent=True, # type: bool + # sampled=None, # type: Optional[bool] + # op=None, # type: Optional[str] + # description=None, # type: Optional[str] + # hub=None, # type: Optional[sentry_sdk.Hub] # deprecated + # status=None, # type: Optional[str] + # containing_transaction=None, # type: Optional[Transaction] + # start_timestamp=None, # type: Optional[Union[datetime, float]] + # scope=None, # type: Optional[sentry_sdk.Scope] + # origin="manual", # type: str ): # type: (...) -> None """ - For backwards compatibility with old the old Span interface, this class + For backwards compatibility with the old Span interface, this class accepts arbitrary keyword arguments, in addition to the ones explicitly listed in the signature. These additional arguments are ignored. """ from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - from sentry_sdk.integrations.opentelemetry.utils import convert_otel_timestamp + from sentry_sdk.integrations.opentelemetry.utils import ( + convert_to_otel_timestamp, + ) if start_timestamp is not None: - start_timestamp = convert_otel_timestamp(start_timestamp) + # OTel timestamps have nanosecond precision + start_timestamp = convert_to_otel_timestamp(start_timestamp) self._otel_span = tracer.start_span( description or op or "", start_time=start_timestamp @@ -1287,6 +1159,35 @@ def __init__( self.scope = scope + try: + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + except AttributeError: + pass + + self._local_aggregator = None # type: Optional[LocalAggregator] + + thread_id, thread_name = get_current_thread_meta() + self.set_thread(thread_id, thread_name) + self.set_profiler_id(get_profiler_id()) + + def __repr__(self): + # type: () -> str + return ( + "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.op, + self.description, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.origin, + ) + ) + def __enter__(self): # type: () -> Span # XXX use_span? https://github.com/open-telemetry/opentelemetry-python/blob/3836da8543ce9751051e38a110c0468724042e62/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547 @@ -1352,7 +1253,7 @@ def origin(self, value): self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, value) @property - def root_span(self): + def segment_span(self): if isinstance(self._otel_span, otel_trace.NonRecordingSpan): return None @@ -1367,7 +1268,7 @@ def root_span(self): return parent @property - def is_root_span(self): + def is_segment(self): if isinstance(self._otel_span, otel_trace.NonRecordingSpan): return False @@ -1379,14 +1280,28 @@ def containing_transaction(self): """ Get the transaction this span is a child of. - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. + .. deprecated:: 3.0.0 + This will be removed in the future. """ - logger.warning( - "Deprecated: use root_span instead. This will be removed in the future." - ) - return self.root_span + logger.warning("Deprecated: This will be removed in the future.") + return self.segment_span + + @containing_transaction.setter + def containing_transaction(self, value): + # type: (Span) -> None + """ + Set this span's transaction. + + .. deprecated:: 3.0.0 + Use :func:`segment_span` instead. + """ + pass + + @property + def parent_span_id(self): + # type: () -> Optional[str] + return self._otel_span.parent if hasattr(self._otel_span, "parent") else None @property def trace_id(self): @@ -1395,6 +1310,7 @@ def trace_id(self): @property def span_id(self): + # type: () -> Optional[str] return self._otel_span.get_span_context().span_id @property @@ -1404,16 +1320,19 @@ def sampled(self): @sampled.setter def sampled(self, value): + # type: () -> Optional[bool] pass @property def op(self): + # type: () -> Optional[str] from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute self._otel_span.attributes.get(SentrySpanAttribute.OP) @op.setter def op(self, value): + # type: (str) -> None from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute self._otel_span.set_attribute(SentrySpanAttribute.OP, value) @@ -1457,7 +1376,9 @@ def from_traceparent( **kwargs, # type: Any ): # type: (...) -> Optional[Span] - pass + # XXX actually propagate + span = Span(**kwargs) + return span def to_traceparent(self): # type: () -> str @@ -1531,10 +1452,12 @@ def is_success(self): def finish(self, scope=None, end_timestamp=None): # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] # XXX check if already finished - from sentry_sdk.integrations.opentelemetry.utils import convert_otel_timestamp + from sentry_sdk.integrations.opentelemetry.utils import ( + convert_to_otel_timestamp, + ) if end_timestamp is not None: - end_timestamp = convert_otel_timestamp(end_timestamp) + end_timestamp = convert_to_otel_timestamp(end_timestamp) self._otel_span.end(end_time=end_timestamp) scope = scope or sentry_sdk.get_current_scope() maybe_create_breadcrumbs_from_span(scope, self) @@ -1551,6 +1474,13 @@ def get_profile_context(self): # type: () -> Optional[ProfileContext] pass + def _get_local_aggregator(self): + # type: (...) -> LocalAggregator + rv = self._local_aggregator + if rv is None: + rv = self._local_aggregator = LocalAggregator() + return rv + # transaction/root span methods def set_context(self, key, value): From 164f66b29f5a771d8313754a4d75028e43cd3089 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Aug 2024 14:52:26 +0200 Subject: [PATCH 15/16] rename and wip --- sentry_sdk/api.py | 7 +- .../opentelemetry/potel_span_processor.py | 8 +- sentry_sdk/integrations/wsgi.py | 6 +- sentry_sdk/scope.py | 78 +++++++++---------- sentry_sdk/tracing.py | 6 +- 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 13d4dedca5..d4fa42a614 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -233,7 +233,7 @@ def flush( def start_span( *, - segment_span=None, + root_span=None, custom_sampling_context=None, **kwargs, # type: Any ): @@ -243,12 +243,11 @@ def start_span( """ # TODO: Consider adding type hints to the method signature. return get_current_scope().start_span( - segment_span, custom_sampling_context, **kwargs + root_span, custom_sampling_context, **kwargs ) def start_transaction( - *, transaction=None, # type: Optional[Span] custom_sampling_context=None, # type: Optional[SamplingContext] **kwargs, # type: Unpack[TransactionKwargs] @@ -288,7 +287,7 @@ def start_transaction( available arguments. """ return get_current_scope().start_span( - segment_span=transaction, + root_span=transaction, custom_sampling_context=custom_sampling_context, **kwargs, ) diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py index e9f76ba817..ebb5bbc17a 100644 --- a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -53,7 +53,7 @@ def on_end(self, span): self._children_spans[span.parent.span_id].append(span) else: # if have a root span ending, we build a transaction and send it - self._flush_segment_span(span) + self._flush_root_span(span) # TODO-neel-potel not sure we need a clear like JS def shutdown(self): @@ -66,9 +66,9 @@ def force_flush(self, timeout_millis=30000): # type: (int) -> bool return True - def _flush_segment_span(self, span): + def _flush_root_span(self, span): # type: (ReadableSpan) -> None - transaction_event = self._segment_span_to_transaction_event(span) + transaction_event = self._root_span_to_transaction_event(span) if not transaction_event: return @@ -103,7 +103,7 @@ def _collect_children(self, span): # we construct the event from scratch here # and not use the current Transaction class for easier refactoring - def _segment_span_to_transaction_event(self, span): + def _root_span_to_transaction_event(self, span): # type: (ReadableSpan) -> Optional[Event] if not span.context: return None diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 2d8308dc98..4ac801ea88 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -91,7 +91,7 @@ def __call__(self, environ, start_response): ) ) - segment_span = continue_trace( + root_span = continue_trace( environ, op=OP.HTTP_SERVER, name="generic WSGI request", @@ -100,13 +100,13 @@ def __call__(self, environ, start_response): ) with sentry_sdk.start_transaction( - segment_span, custom_sampling_context={"wsgi_environ": environ} + root_span, custom_sampling_context={"wsgi_environ": environ} ): try: response = self.app( environ, partial( - _sentry_start_response, start_response, segment_span + _sentry_start_response, start_response, root_span ), ) except BaseException: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 8fcd972c53..3da70d20e7 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -166,8 +166,8 @@ class Scope(object): "_level", "_name", "_fingerprint", - "_segment_span_name", - "_segment_span_info", + "_root_span_name", + "_root_span_info", "_user", "_tags", "_contexts", @@ -219,8 +219,8 @@ def __copy__(self): rv._level = self._level rv._name = self._name rv._fingerprint = self._fingerprint - rv._segment_span_name = self._segment_span_name - rv._segment_span_info = dict(self._segment_span_info) + rv._root_span_name = self._root_span_name + rv._root_span_info = dict(self._root_span_info) rv._user = self._user rv._tags = dict(self._tags) @@ -670,8 +670,8 @@ def clear(self): """Clears the entire scope.""" self._level = None # type: Optional[LogLevelStr] self._fingerprint = None # type: Optional[List[str]] - self._segment_span_name = None # type: Optional[str] - self._segment_span_info = {} # type: MutableMapping[str, str] + self._root_span_name = None # type: Optional[str] + self._root_span_info = {} # type: MutableMapping[str, str] self._user = None # type: Optional[Dict[str, Any]] self._tags = {} # type: Dict[str, Any] @@ -716,29 +716,29 @@ def transaction(self): Return the transaction in the scope, if any. .. deprecated:: 3.0.0 - This function is deprecated and will be removed in a future release. Use Scope.segment_span instead. + This function is deprecated and will be removed in a future release. Use Scope.root_span instead. """ logger.warning( - "Deprecated: use Scope.segment_span instead. This will be removed in the future." + "Deprecated: use Scope.root_span instead. This will be removed in the future." ) - return self.segment_span + return self.root_span @property - def segment_span(self): + def root_span(self): """Return the root span in the scope, if any.""" # there is no span/transaction on the scope if self._span is None: return None # there is an orphan span on the scope - if self._span.segment_span is None: + if self._span.root_span is None: return None # there is either a root span (which is its own containing # root span) or a non-orphan span on the scope - return self._span.segment_span + return self._span.root_span def set_transaction_name(self, name, source=None): # type: (str, Optional[str]) -> None @@ -746,19 +746,19 @@ def set_transaction_name(self, name, source=None): Set the transaction name and optionally the transaction source. .. deprecated:: 3.0.0 - This function is deprecated and will be removed in a future release. Use Scope.set_segment_span_name instead. + This function is deprecated and will be removed in a future release. Use Scope.set_root_span_name instead. """ - self.set_segment_span_name(name, source) + self.set_root_span_name(name, source) - def set_segment_span_name(self, name, source=None): + def set_root_span_name(self, name, source=None): """Set the root span name and optionally the source.""" - self._segment_span_name = name - if self._span and self._span.segment_span: - self._span.segment_span.name = name + self._root_span_name = name + if self._span and self._span.root_span: + self._span.root_span.name = name if source: - self._span.segment_span.source = source + self._span.root_span.source = source if source: - self._segment_span_info["source"] = source + self._root_span_info["source"] = source @_attr_setter def user(self, value): @@ -790,9 +790,9 @@ def span(self, span): return if span.is_segment: if span.name: - self._segment_span_name = span.name + self._root_span_name = span.name if span.source: - self._segment_span_info["source"] = span.source + self._root_span_info["source"] = span.source @property def profile(self): @@ -1017,7 +1017,7 @@ def start_transaction( return span - def start_span(self, segment_span=None, custom_sampling_context=None, **kwargs): + def start_span(self, root_span=None, custom_sampling_context=None, **kwargs): # type: (Optional[Span], Optional[SamplingContext], Any) -> Span """ Start a span whose parent is the currently active span or transaction, if any. @@ -1029,8 +1029,8 @@ def start_span(self, segment_span=None, custom_sampling_context=None, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ kwargs.setdefault("scope", self) - if segment_span: - return segment_span + if root_span: + return root_span span = self.span or self.get_isolation_scope().span @@ -1074,7 +1074,7 @@ def continue_trace( """ self.generate_propagation_context(environ_or_headers) - segment_span = Span.continue_from_headers( + root_span = Span.continue_from_headers( normalize_incoming_data(environ_or_headers), op=op, origin=origin, @@ -1082,7 +1082,7 @@ def continue_trace( source=source, ) - return segment_span + return root_span def capture_event(self, event, hint=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str] @@ -1286,18 +1286,18 @@ def _apply_user_to_event(self, event, hint, options): if event.get("user") is None and self._user is not None: event["user"] = self._user - def _apply_segment_span_name_to_event(self, event, hint, options): + def _apply_root_span_name_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction") is None and self._segment_span_name is not None: - event["transaction"] = self._segment_span_name + if event.get("transaction") is None and self._root_span_name is not None: + event["transaction"] = self._root_span_name - def _apply_segment_span_info_to_event(self, event, hint, options): + def _apply_root_span_info_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None if ( event.get("transaction_info") is None - and self._segment_span_info is not None + and self._root_span_info is not None ): - event["transaction_info"] = self._segment_span_info + event["transaction_info"] = self._root_span_info def _apply_fingerprint_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None @@ -1419,8 +1419,8 @@ def apply_to_event( self._apply_level_to_event(event, hint, options) self._apply_fingerprint_to_event(event, hint, options) self._apply_user_to_event(event, hint, options) - self._apply_segment_span_name_to_event(event, hint, options) - self._apply_segment_span_info_to_event(event, hint, options) + self._apply_root_span_name_to_event(event, hint, options) + self._apply_root_span_info_to_event(event, hint, options) self._apply_tags_to_event(event, hint, options) self._apply_extra_to_event(event, hint, options) @@ -1444,10 +1444,10 @@ def update_from_scope(self, scope): self._level = scope._level if scope._fingerprint is not None: self._fingerprint = scope._fingerprint - if scope._segment_span_name is not None: - self._segment_span_name = scope._segment_span_name - if scope._segment_span_info is not None: - self._segment_span_info.update(scope._segment_span_info) + if scope._root_span_name is not None: + self._root_span_name = scope._root_span_name + if scope._root_span_info is not None: + self._root_span_info.update(scope._root_span_info) if scope._user is not None: self._user = scope._user if scope._tags: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 529b7958bc..3a2f3d4ae2 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1253,7 +1253,7 @@ def origin(self, value): self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, value) @property - def segment_span(self): + def root_span(self): if isinstance(self._otel_span, otel_trace.NonRecordingSpan): return None @@ -1285,7 +1285,7 @@ def containing_transaction(self): """ logger.warning("Deprecated: This will be removed in the future.") - return self.segment_span + return self.root_span @containing_transaction.setter def containing_transaction(self, value): @@ -1294,7 +1294,7 @@ def containing_transaction(self, value): Set this span's transaction. .. deprecated:: 3.0.0 - Use :func:`segment_span` instead. + Use :func:`root_span` instead. """ pass From f753c21d6b2e81172ab7103cd9e2b01d0cd68331 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 9 Aug 2024 10:38:32 +0200 Subject: [PATCH 16/16] ? --- sentry_sdk/api.py | 4 +--- sentry_sdk/scope.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index d4fa42a614..a027dcc144 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -242,9 +242,7 @@ def start_span( Start and return a span. """ # TODO: Consider adding type hints to the method signature. - return get_current_scope().start_span( - root_span, custom_sampling_context, **kwargs - ) + return get_current_scope().start_span(root_span, custom_sampling_context, **kwargs) def start_transaction( diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 3da70d20e7..4382ff48bf 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1293,10 +1293,7 @@ def _apply_root_span_name_to_event(self, event, hint, options): def _apply_root_span_info_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if ( - event.get("transaction_info") is None - and self._root_span_info is not None - ): + if event.get("transaction_info") is None and self._root_span_info is not None: event["transaction_info"] = self._root_span_info def _apply_fingerprint_to_event(self, event, hint, options):