diff --git a/docs/api.rst b/docs/api.rst index b2925ef33f..a790054f4f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -39,9 +39,9 @@ Performance Monitoring .. autofunction:: sentry_sdk.api.trace .. autofunction:: sentry_sdk.api.continue_trace +.. autofunction:: sentry_sdk.api.new_trace .. autofunction:: sentry_sdk.api.get_current_span .. autofunction:: sentry_sdk.api.start_span -.. autofunction:: sentry_sdk.api.start_transaction Distributed Tracing diff --git a/docs/apidocs.rst b/docs/apidocs.rst index 4991b8ccd6..dd16a59233 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -23,10 +23,10 @@ API Docs .. autoclass:: sentry_sdk.HttpTransport :members: -.. autoclass:: sentry_sdk.tracing.Transaction +.. autoclass:: sentry_sdk.traces.StreamedSpan :members: -.. autoclass:: sentry_sdk.tracing.Span +.. autoclass:: sentry_sdk.traces.NoOpStreamedSpan :members: .. autoclass:: sentry_sdk.session.Session diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 28e79202d8..417220ecc1 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -22,6 +22,7 @@ "capture_exception", "capture_message", "continue_trace", + "new_trace", "flush", "flush_async", "get_baggage", @@ -45,7 +46,6 @@ "set_tags", "set_user", "start_span", - "start_transaction", "trace", "monitor", "logger", diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 8cd412213e..98dff9e88c 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -383,7 +383,6 @@ class DataCollection(TypedDict): EventProcessor = Callable[[Event, Hint], Optional[Event]] ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] - TransactionProcessor = Callable[[Event, Hint], Optional[Event]] LogProcessor = Callable[[Log, Hint], Optional[Log]] TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index dfadc290db..6ce400cba4 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -24,8 +24,7 @@ def record_token_usage( if input_tokens_cached is not None: span.set_attribute( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - input_tokens_cached, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, input_tokens_cached ) if input_tokens_cache_write is not None: diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 7446b8613b..3d2baef89c 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,11 +1,11 @@ import inspect from typing import TYPE_CHECKING -from sentry_sdk import Client, tracing_utils +from sentry_sdk import Client, traces from sentry_sdk._init_implementation import init from sentry_sdk.crons import monitor from sentry_sdk.scope import Scope, isolation_scope, new_scope -from sentry_sdk.tracing import NoOpSpan, Transaction, trace +from sentry_sdk.traces import trace if TYPE_CHECKING: from collections.abc import Mapping @@ -19,8 +19,6 @@ overload, ) - from typing_extensions import Unpack - from sentry_sdk._types import ( Breadcrumb, BreadcrumbHint, @@ -28,10 +26,9 @@ ExcInfo, Hint, LogLevelStr, - SamplingContext, ) from sentry_sdk.client import BaseClient - from sentry_sdk.tracing import Span, TransactionKwargs + from sentry_sdk.traces import StreamedSpan T = TypeVar("T") F = TypeVar("F", bound=Callable[..., Any]) @@ -50,6 +47,7 @@ def overload(x: "T") -> "T": "capture_exception", "capture_message", "continue_trace", + "new_trace", "flush", "flush_async", "get_baggage", @@ -73,7 +71,6 @@ def overload(x: "T") -> "T": "set_tags", "set_user", "start_span", - "start_transaction", "trace", "monitor", "start_session", @@ -273,59 +270,15 @@ async def flush_async( @scopemethod -def start_span( - **kwargs: "Any", -) -> "Span": - return get_current_scope().start_span(**kwargs) - - -@scopemethod -def start_transaction( - transaction: "Optional[Transaction]" = None, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", -) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction on the current scope. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. +def start_span(**kwargs: "Any") -> "StreamedSpan": + return traces.start_span(**kwargs) - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - return get_current_scope().start_transaction( - transaction, custom_sampling_context, **kwargs - ) - - -def get_current_span( - scope: "Optional[Scope]" = None, -) -> "Optional[Span]": +def get_current_span(scope: "Optional[Scope]" = None) -> "Optional[StreamedSpan]": """ Returns the currently active span if there is one running, otherwise `None` """ - return tracing_utils.get_current_span(scope) + return traces.get_current_span(scope) def get_traceparent() -> "Optional[str]": @@ -346,19 +299,18 @@ def get_baggage() -> "Optional[str]": return None -def continue_trace( - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", -) -> "Transaction": +def continue_trace(incoming: "Dict[str, Any]") -> None: """ - Sets the propagation context from environment or headers and returns a transaction. + Sets the propagation context from environment or headers. """ - return get_isolation_scope().continue_trace( - environ_or_headers, op, name, source, origin - ) + return traces.continue_trace(incoming) + + +def new_trace() -> None: + """ + Resets the propagation context, forcing a new trace. + """ + return traces.new_trace() @scopemethod diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ce184ffd44..548d2cc0d0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -27,7 +27,7 @@ _map_from_send_default_pii, _resolve_data_collection, ) -from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.envelope import Envelope, Item from sentry_sdk.integrations import setup_integrations from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.monitor import Monitor @@ -35,10 +35,7 @@ from sentry_sdk.scrubber import EventScrubber from sentry_sdk.serializer import serialize from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.traces import trace as streaming_trace -from sentry_sdk.tracing import trace as legacy_trace -from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.traces import SpanStatus, StreamedSpan, trace from sentry_sdk.transport import ( AsyncHttpTransport, HttpTransportCore, @@ -364,16 +361,6 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False ) - if rv["ignore_spans"] and not has_span_streaming_enabled(rv): - logger.warning( - "The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.", - ) - - if rv["before_send_span"] and not has_span_streaming_enabled(rv): - logger.warning( - "The `before_send_span` parameter only works when `trace_lifecycle` is set to `stream`.", - ) - return rv @@ -507,12 +494,6 @@ def _setup_instrumentation( """ Instruments the functions given in the list `functions_to_trace` with a trace decorator. """ - trace = ( - streaming_trace - if has_span_streaming_enabled(self.options) - else legacy_trace - ) - for function in functions_to_trace: class_name = None function_qualname = function["qualified_name"] @@ -635,12 +616,10 @@ def _record_lost_event( record_lost_func=_record_lost_event, ) - self.span_batcher = None - if has_span_streaming_enabled(self.options): - self.span_batcher = SpanBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) max_request_body_size = ("always", "never", "small", "medium") if self.options["max_request_body_size"] not in max_request_body_size: @@ -721,16 +700,12 @@ def _prepare_event( hint: "Hint", scope: "Optional[Scope]", ) -> "Optional[Event]": - previous_total_spans: "Optional[int]" = None previous_total_breadcrumbs: "Optional[int]" = None if event.get("timestamp") is None: event["timestamp"] = datetime.now(timezone.utc) - is_transaction = event.get("type") == "transaction" - if scope is not None: - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) event_ = scope.apply_to_event(event, hint, self.options) # one of the event/error processors returned None @@ -738,37 +713,12 @@ def _prepare_event( if self.transport: self.transport.record_lost_event( "event_processor", - data_category=("transaction" if is_transaction else "error"), + data_category="error", ) - if is_transaction: - self.transport.record_lost_event( - "event_processor", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) return None event = event_ - spans_delta = spans_before - len( - cast(List[Dict[str, object]], event.get("spans", [])) - ) - span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - - if is_transaction and self.transport is not None: - if spans_delta > 0: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) - if span_recorder_dropped_spans > 0: - self.transport.record_lost_event( - "buffer_overflow", - data_category="span", - quantity=span_recorder_dropped_spans, - ) - dropped_spans: int = span_recorder_dropped_spans + spans_delta - if dropped_spans > 0: - previous_total_spans = spans_before + dropped_spans if scope._n_breadcrumbs_truncated > 0: breadcrumbs = event.get("breadcrumbs", {}) values = ( @@ -781,8 +731,7 @@ def _prepare_event( ) if ( - not is_transaction - and self.options["attach_stacktrace"] + self.options["attach_stacktrace"] and "exception" not in event and "stacktrace" not in event and "threads" not in event @@ -843,10 +792,7 @@ def _prepare_event( span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], {"len": scope._gen_ai_original_message_count[span_id]}, ) - if previous_total_spans is not None: - event["spans"] = AnnotatedValue( - event.get("spans", []), {"len": previous_total_spans} - ) + if previous_total_breadcrumbs is not None: event["breadcrumbs"] = AnnotatedValue( event.get("breadcrumbs", {"values": []}), @@ -867,11 +813,7 @@ def _prepare_event( ) before_send = self.options["before_send"] - if ( - before_send is not None - and event is not None - and event.get("type") != "transaction" - ): + if before_send is not None and event is not None: new_event = None with capture_internal_exceptions(): new_event = before_send(event, hint or {}) @@ -891,36 +833,6 @@ def _prepare_event( event = new_event - before_send_transaction = self.options["before_send_transaction"] - if ( - before_send_transaction is not None - and event is not None - and event.get("type") == "transaction" - ): - new_event = None - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - with capture_internal_exceptions(): - new_event = before_send_transaction(event, hint or {}) - if new_event is None: - logger.info("before send transaction dropped event") - if self.transport: - self.transport.record_lost_event( - reason="before_send", data_category="transaction" - ) - self.transport.record_lost_event( - reason="before_send", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - else: - spans_delta = spans_before - len(new_event.get("spans", [])) - if spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - reason="before_send", data_category="span", quantity=spans_delta - ) - - event = new_event - return event def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: @@ -950,11 +862,6 @@ def _should_capture( hint: "Hint", scope: "Optional[Scope]" = None, ) -> bool: - # Transactions are sampled independent of error events. - is_transaction = event.get("type") == "transaction" - if is_transaction: - return True - ignoring_prevents_recursion = scope is not None and not scope._should_capture if ignoring_prevents_recursion: return False @@ -1074,7 +981,6 @@ def capture_event( if event_id is None: event["event_id"] = event_id = uuid.uuid4().hex - span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) event_opt = self._prepare_event(event, hint, scope) if event_opt is None: return None @@ -1085,14 +991,9 @@ def capture_event( if session: self._update_session_from_event(session, event) - is_transaction = event_opt.get("type") == "transaction" is_checkin = event_opt.get("type") == "check_in" - if ( - not is_transaction - and not is_checkin - and not self._should_sample_error(event, hint) - ): + if not is_checkin and not self._should_sample_error(event, hint): return None attachments = hint.get("attachments") @@ -1110,41 +1011,7 @@ def capture_event( envelope = Envelope(headers=headers) - if is_transaction and not span_recorder_has_gen_ai_span: - envelope.add_transaction(event_opt) - elif is_transaction: - split_spans = _split_gen_ai_spans(event_opt) - if split_spans is None or not split_spans[1]: - envelope.add_transaction(event_opt) - else: - non_gen_ai_spans, gen_ai_spans = split_spans - - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) - - converted_gen_ai_spans = [ - _serialized_v1_span_to_serialized_v2_span(span, event_opt) - for span in gen_ai_spans - if isinstance(span, dict) - ] - - envelope.add_item( - Item( - type=SpanBatcher.TYPE, - content_type=SpanBatcher.CONTENT_TYPE, - headers={ - "item_count": len(converted_gen_ai_spans), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": converted_gen_ai_spans, - }, - ), - ) - ) - - elif is_checkin: + if is_checkin: envelope.add_checkin(event_opt) else: envelope.add_event(event_opt) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7e7a25f65d..b1c714d6e4 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -55,7 +55,6 @@ class CompressionAlgo(Enum): Metric, SpanJSON, TracesSampler, - TransactionProcessor, ) # Experiments are feature flags to enable and disable certain unstable SDK @@ -1310,7 +1309,6 @@ def __init__( send_client_reports: bool = True, _experiments: "Experiments" = {}, # noqa: B006 proxy_headers: "Optional[Dict[str, str]]" = None, - before_send_transaction: "Optional[TransactionProcessor]" = None, project_root: "Optional[str]" = None, include_local_variables: "Optional[bool]" = True, include_source_context: "Optional[bool]" = True, @@ -1548,11 +1546,6 @@ def __init__( By the time `before_send` is executed, all scope data has already been applied to the event. Further modification of the scope won't have any effect. - :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can - return a modified transaction event object, or `null` to skip reporting the event. - - One way this might be used is for manual PII stripping before sending. - :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb is added to the scope. diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index c4e3a4d42d..85a5a4c18f 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -23,7 +23,7 @@ class Envelope: """ Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, - each envelope may have at most one Item with type "event" or "transaction" (but not both). + each envelope may have at most one Item with type "event". """ def __init__( @@ -53,12 +53,6 @@ def add_event( ) -> None: self.add_item(Item(payload=PayloadRef(json=event), type="event")) - def add_transaction( - self, - transaction: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - def add_profile_chunk( self, profile_chunk: "Any", @@ -104,13 +98,6 @@ def get_event(self) -> "Optional[Event]": return event return None - def get_transaction_event(self) -> "Optional[Event]": - for item in self.items: - event = item.get_transaction_event() - if event is not None: - return event - return None - def __iter__(self) -> "Iterator[Item]": return iter(self.items) @@ -241,8 +228,6 @@ def data_category(self) -> "EventDataCategory": return "session" elif ty == "attachment": return "attachment" - elif ty == "transaction": - return "transaction" elif ty == "span": return "span" elif ty == "event": @@ -271,11 +256,6 @@ def get_event(self) -> "Optional[Event]": return self.payload.json return None - def get_transaction_event(self) -> "Optional[Event]": - if self.type == "transaction" and self.payload.json is not None: - return self.payload.json - return None - def serialize_into( self, f: "Any", @@ -310,7 +290,7 @@ def deserialize_from( # if no length was specified we need to read up to the end of line # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) payload = f.readline().rstrip(b"\n") - if headers.get("type") in ("event", "transaction"): + if headers.get("type") == "event": rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) else: rv = cls(headers=headers, payload=payload) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 76ec2cda2a..23e5c9aca6 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -9,7 +9,7 @@ from enum import Enum from functools import wraps from itertools import chain -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk._types import AnnotatedValue @@ -20,11 +20,6 @@ SPANDATA, ) from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) from sentry_sdk.session import Session from sentry_sdk.traces import ( _DEFAULT_PARENT_SPAN, @@ -34,15 +29,11 @@ from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, - NoOpSpan, - Span, - Transaction, ) from sentry_sdk.tracing_utils import ( Baggage, PropagationContext, _make_sampling_decision, - has_span_streaming_enabled, has_tracing_enabled, is_ignored_span, ) @@ -75,8 +66,6 @@ Union, ) - from typing_extensions import Unpack - import sentry_sdk from sentry_sdk._types import ( Attributes, @@ -91,10 +80,8 @@ Log, LogLevelStr, Metric, - SamplingContext, Type, ) - from sentry_sdk.tracing import TransactionKwargs P = ParamSpec("P") R = TypeVar("R") @@ -585,12 +572,9 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": if not has_tracing_enabled(client.options): return self.get_active_propagation_context().to_traceparent() - span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return traceparent from there - if span_streaming and self.streamed_span is not None: + if self.streamed_span is not None: return self.streamed_span._to_traceparent() - elif not span_streaming and self.span is not None: - return self.span._to_traceparent() # else return traceparent from the propagation context return self.get_active_propagation_context().to_traceparent() @@ -605,12 +589,9 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": if not has_tracing_enabled(client.options): return self.get_active_propagation_context().get_baggage() - span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return baggage from there - if span_streaming and self.streamed_span is not None: + if self.streamed_span is not None: return self.streamed_span._to_baggage() - elif not span_streaming and self.span is not None: - return self.span._to_baggage() # else return baggage from the propagation context return self.get_active_propagation_context().get_baggage() @@ -619,11 +600,7 @@ def get_trace_context(self) -> "Dict[str, Any]": """ Returns the Sentry "trace" context from the Propagation Context. """ - if ( - has_tracing_enabled(self.get_client().options) - and self._span is not None - and not isinstance(self._span, NoOpSpan) - ): + if has_tracing_enabled(self.get_client().options) and self._span is not None: return self._span._get_trace_context() # if we are tracing externally (otel), those values take precedence @@ -664,16 +641,9 @@ def iter_trace_propagation_headers( """ client = self.get_client() - span = kwargs.pop("span", None) - if not span: - span_streaming = has_span_streaming_enabled(client.options) - span = self.streamed_span if span_streaming else self.span + span = kwargs.pop("span", None) or self.streamed_span - if ( - has_tracing_enabled(client.options) - and span is not None - and not isinstance(span, NoOpSpan) - ): + if has_tracing_enabled(client.options) and span is not None: for header in span._iter_headers(): yield header elif has_external_propagation_context(): @@ -722,7 +692,7 @@ def clear(self) -> None: self.clear_breadcrumbs() self._should_capture: bool = True - self._span: "Optional[Union[Span, StreamedSpan]]" = None + self._span: "Optional[StreamedSpan]" = None self._session: "Optional[Session]" = None self._force_auto_session_tracking: "Optional[bool]" = None @@ -830,23 +800,6 @@ def set_user(self, value: "Optional[Dict[str, Any]]") -> None: if session is not None: session.update(user=value) - @property - def span(self) -> "Optional[Span]": - """Get/set current tracing span or transaction.""" - return self._span if isinstance(self._span, Span) else None - - @span.setter - def span(self, span: "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 - @property def streamed_span(self) -> "Optional[StreamedSpan]": """Get/set current tracing span.""" @@ -1041,160 +994,13 @@ def add_breadcrumb( self._breadcrumbs.popleft() self._n_breadcrumbs_truncated += 1 - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - client = self.get_client() - if has_span_streaming_enabled(client.options): - deprecation_warning( - "Scope.start_transaction is not available in streaming mode.", - ) - return NoOpSpan() - - kwargs.setdefault("scope", self) - - 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: "TransactionKwargs" = kwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_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) - - # update the sample rate in the dsc - if transaction.sample_rate is not None: - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction._baggage: - transaction._baggage.sentry_items["sample_rate"] = str( - transaction.sample_rate - ) - - if transaction.sampled: - transaction._continuous_profile = try_profile_lifecycle_trace_start() - - # Typically, the profiler is set when the transaction is created. But when - # using the auto lifecycle, the profiler isn't running when the first - # transaction is started. So make sure we update the profiler id on it. - if transaction._continuous_profile is not None: - transaction.set_profiler_id(get_profiler_id()) - - # 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) - - return transaction - - def start_span(self, **kwargs: "Any") -> "Span": - """ - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - 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`. - """ - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - deprecation_warning( - "Scope.start_span is not available in streaming mode.", - ) - return NoOpSpan() - - if kwargs.get("description") is not None: - deprecation_warning( - "The `description` parameter is deprecated. Please use `name` instead.", - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - client = self.get_client() - - # get current span or transaction - span = self.span or self.get_isolation_scope().span - if isinstance(span, StreamedSpan): - # make mypy happy - return NoOpSpan() - - 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() - 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 - - def start_streamed_span( + def start_span( self, name: str, attributes: "Optional[Attributes]", parent_span: "Optional[StreamedSpan]", active: bool, ) -> "StreamedSpan": - # TODO: rename to start_span once we drop the old API if isinstance(parent_span, NoOpStreamedSpan): # parent_span is only set if the user explicitly set it logger.debug( @@ -1308,39 +1114,6 @@ def _update_sample_rate(self, sample_rate: float) -> None: if baggage is not None and baggage.sentry_items.get("sample_rate"): baggage.sentry_items["sample_rate"] = str(sample_rate) - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", - ) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - self.generate_propagation_context(environ_or_headers) - - # generate_propagation_context ensures that the propagation_context is not None. - propagation_context = cast(PropagationContext, self._propagation_context) - - optional_kwargs = {} - if name: - optional_kwargs["name"] = name - if source: - optional_kwargs["source"] = source - - return Transaction( - op=op, - origin=origin, - baggage=propagation_context.baggage, - parent_sampled=propagation_context.parent_sampled, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - same_process_as_parent=False, - **optional_kwargs, - ) - def capture_event( self, event: "Event", @@ -1373,7 +1146,7 @@ def capture_event( event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) - if event_id is not None and event.get("type") != "transaction": + if event_id is not None: self.get_isolation_scope()._last_event_id = event_id return event_id @@ -1414,11 +1187,9 @@ def _capture_span(self, span: "Optional[StreamedSpan]") -> None: if span is None: return - client = self.get_client() - if not has_span_streaming_enabled(client.options): - return - merged_scope = self._merge_scopes() + + client = self.get_client() client._capture_span(span, scope=merged_scope) def capture_message( @@ -1768,7 +1539,6 @@ def apply_to_event( ) -> "Optional[Event]": """Applies the information contained on the scope to the given event.""" ty = event.get("type") - is_transaction = ty == "transaction" is_check_in = ty == "check_in" # put all attachments into the hint. This lets callbacks play around @@ -1776,8 +1546,7 @@ def apply_to_event( # create the envelope. attachments_to_send = hint.get("attachments") or [] for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: - attachments_to_send.append(attachment) + attachments_to_send.append(attachment) hint["attachments"] = attachments_to_send self._apply_contexts_to_event(event, hint, options) @@ -1796,8 +1565,6 @@ def apply_to_event( self._apply_transaction_info_to_event(event, hint, options) self._apply_tags_to_event(event, hint, options) self._apply_extra_to_event(event, hint, options) - - if not is_transaction and not is_check_in: self._apply_breadcrumbs_to_event(event, hint, options) self._apply_flags_to_event(event, hint, options) @@ -1826,7 +1593,7 @@ def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> N # isn't one if telemetry.get("span_id") is None: if self._span is not None and not isinstance( - self._span, (NoOpStreamedSpan, NoOpSpan) + self._span, NoOpStreamedSpan ): telemetry["span_id"] = self._span.span_id else: diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 32e7b812b1..fa71c31d44 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -1,23 +1,3 @@ -""" -The API in this file is only meant to be used in span streaming mode. It should -not be mixed with the legacy tracing API (sentry_sdk.start_transaction, -sentry_sdk.start_span, etc.). - -You can enable span streaming mode via: - -``` -import sentry_sdk - -sentry_sdk.init( - trace_lifecycle="stream", -) -``` - -See -https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide/ -for how to migrate to span streaming. -""" - import sys import uuid from datetime import datetime, timedelta, timezone @@ -120,7 +100,7 @@ def start_span( active: bool = True, ) -> "StreamedSpan": """ - Start a span in streaming mode. + Start a span. The span's parent, unless provided explicitly via the `parent_span` argument, will be the current active span, if any. If there is none, this span will @@ -173,25 +153,14 @@ def start_span( :return: The span that has been started. :rtype: StreamedSpan """ - from sentry_sdk.tracing_utils import has_span_streaming_enabled - - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - logger.warning( - "Using span streaming API in non-span-streaming mode. Use " - "sentry_sdk.start_transaction() and sentry_sdk.start_span() " - "instead.", - ) - return NoOpStreamedSpan() - - return sentry_sdk.get_current_scope().start_streamed_span( + return sentry_sdk.get_current_scope().start_span( name, attributes, parent_span, active ) def continue_trace(incoming: "dict[str, Any]") -> None: """ - Continue a trace from headers or environment variables in streaming mode. + Continue a trace from headers or environment variables. This function sets the propagation context on the scope. Any span started in the updated scope will belong under the trace extracted from the @@ -215,7 +184,7 @@ def continue_trace(incoming: "dict[str, Any]") -> None: def new_trace() -> None: """ - Resets the propagation context, forcing a new trace, in streaming mode. + Resets the propagation context, forcing a new trace. This function sets the propagation context on the scope. Any span started in the updated scope will start its own trace. @@ -859,10 +828,10 @@ def make_db_query(sql): pass """ from sentry_sdk.tracing_utils import ( - create_streaming_span_decorator, + create_span_decorator, ) - decorator = create_streaming_span_decorator( + decorator = create_span_decorator( name=name, attributes=attributes, active=active, diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index e101217fc3..045e64160f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,117 +1,19 @@ -import uuid -from datetime import datetime, timedelta, timezone from enum import Enum -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING -import sentry_sdk -from sentry_sdk.consts import SPANDATA, SPANSTATUS -from sentry_sdk.profiler.continuous_profiler import get_profiler_id -from sentry_sdk.utils import ( - capture_internal_exceptions, - deprecation_warning, - get_current_thread_meta, - is_valid_sample_rate, - logger, - nanosecond_time, - should_be_treated_as_error, -) +from sentry_sdk.consts import SPANSTATUS if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping from typing import ( - Any, - Dict, - Iterator, - List, - Optional, ParamSpec, - Tuple, TypeVar, - Union, - overload, ) - from typing_extensions import TypedDict, Unpack + from typing_extensions import TypedDict P = ParamSpec("P") R = TypeVar("R") - from sentry_sdk._types import ( - Event, - SamplingContext, - ) - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - - class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - - op: str - """ - The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - """ - - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - - status: str - """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - - containing_transaction: "Optional[Transaction]" - """The transaction that this span belongs to.""" - - start_timestamp: "Optional[Union[datetime, float]]" - """ - The timestamp when the span started. If omitted, the current time - will be used. - """ - - scope: "sentry_sdk.Scope" - """The scope to use for this span. If not provided, we use the current scope.""" - - origin: str - """ - The origin of the span. - See https://develop.sentry.dev/sdk/performance/trace-origin/ - Default "manual". - """ - - name: str - """A string describing what operation is being performed within the span/transaction.""" - - class TransactionKwargs(SpanKwargs, total=False): - source: str - """ - 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". - """ - - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - ProfileContext = TypedDict( "ProfileContext", { @@ -193,1126 +95,4 @@ def get_span_status_from_http_code(http_status_code: int) -> str: return SPANSTATUS.UNKNOWN_ERROR -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans", "dropped_spans") - - def __init__(self, maxlen: int) -> None: - # FIXME: this is `maxlen - 1` only to preserve historical behavior - # enforced by tests. - # Either this should be changed to `maxlen` or the JS SDK implementation - # should be changed to match a consistent interpretation of what maxlen - # limits: either transaction+spans or only child spans. - self.maxlen = maxlen - 1 - self.spans: "List[Span]" = [] - self.dropped_spans: int = 0 - - def add(self, span: "Span") -> None: - if len(self.spans) > self.maxlen: - span._span_recorder = None - self.dropped_spans += 1 - else: - 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. - - .. deprecated:: 2.15.0 - Please use the `name` parameter, instead. - :param name: A string describing what operation is being performed within the 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", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - "_flags", - "_flags_capacity", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - same_process_as_parent: bool = True, - sampled: "Optional[bool]" = None, - op: "Optional[str]" = None, - description: "Optional[str]" = None, - status: "Optional[str]" = None, - containing_transaction: "Optional[Transaction]" = None, - start_timestamp: "Optional[Union[datetime, float]]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - origin: str = "manual", - name: "Optional[str]" = None, - ) -> None: - self._trace_id = trace_id - self._span_id = span_id - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = name or description - self.status = status - self.scope = scope - self.origin = origin - self._tags: "MutableMapping[str, str]" = {} - self._data: "Dict[str, Any]" = {} - self._containing_transaction = containing_transaction - self._flags: "Dict[str, bool]" = {} - self._flags_capacity = 10 - - 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: "Optional[datetime]" = None - - self._span_recorder: "Optional[_SpanRecorder]" = None - - self.update_active_thread() - 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: int) -> None: - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - def __repr__(self) -> 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) -> "Span": - if has_span_streaming_enabled(sentry_sdk.get_client().options): - self._context_manager_state = None # early return sentinel - return self - - 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: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if ( - hasattr(self, "_context_manager_state") - and self._context_manager_state is None - ): - del self._context_manager_state - return - - if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - with capture_internal_exceptions(): - scope, old_span = cast( - "Tuple[sentry_sdk.Scope, Optional[Span]]", - self._context_manager_state, - ) - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self) -> "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: "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. - """ - if kwargs.get("description") is not None: - deprecation_warning( - "The `description` parameter is deprecated. Please use `name` instead.", - ) - - 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: "Mapping[str, str]", - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - 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. - """ - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers: "Mapping[str, str]", - *, - _sample_rand: "Optional[str]" = None, - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - 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. - :param _sample_rand: If provided, we override the sample_rand value from the - incoming headers with this value. (internal use only) - """ - logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") - - # 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), _sample_rand=_sample_rand - ) - 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) -> "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: "Optional[str]", - **kwargs: "Any", - ) -> "Optional[Transaction]": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self) -> 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) -> "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: str, value: "Any") -> None: - self._tags[key] = value - - def set_data(self, key: str, value: "Any") -> None: - self._data[key] = value - - def update_data(self, data: "Dict[str, Any]") -> None: - self._data.update(data) - - def set_flag(self, flag: str, result: bool) -> None: - if len(self._flags) < self._flags_capacity: - self._flags[flag] = result - - def set_status(self, value: str) -> None: - self.status = value - - def set_thread( - self, thread_id: "Optional[int]", thread_name: "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: "Optional[str]") -> None: - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status: int) -> None: - self.set_tag( - "http.status_code", str(http_status) - ) # TODO-neel remove in major, 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) -> bool: - return self.status == "ok" - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "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() - - # Copy conversation_id from scope to span data if this is an AI span - conversation_id = scope.get_conversation_id() - if conversation_id: - has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data - is_ai_span_op = self.op is not None and ( - self.op.startswith("ai.") or self.op.startswith("gen_ai.") - ) - if has_ai_op or is_ai_span_op: - self.set_data("gen_ai.conversation.id", conversation_id) - - return None - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the span.""" - - rv: "Dict[str, Any]" = { - "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, - } - - if self.status: - rv["status"] = self.status - # TODO-neel remove redundant tag in major - self._tags["status"] = self.status - - tags = self._tags - if tags: - rv["tags"] = tags - - data = {} - data.update(self._flags) - data.update(self._data) - if data: - rv["data"] = data - - return rv - - def get_trace_context(self) -> "Any": - rv: "Dict[str, Any]" = { - "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, - } - 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) -> "Optional[ProfileContext]": - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - def update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -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", - "_contexts", - "_continuous_profile", - "_baggage", - "_sample_rand", - ) - - def __init__( # type: ignore[misc] - self, - name: str = "", - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - source: str = TransactionSource.CUSTOM, - **kwargs: "Unpack[SpanKwargs]", - ) -> None: - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate: "Optional[float]" = None - self.parent_sampled = parent_sampled - self._contexts: "Dict[str, Any]" = {} - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._baggage = baggage - - baggage_sample_rand = ( - None if self._baggage is None else self._baggage._sample_rand() - ) - if baggage_sample_rand is not None: - self._sample_rand = baggage_sample_rand - else: - self._sample_rand = _generate_sample_rand(self.trace_id) - - def __repr__(self) -> 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) -> 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) -> "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__() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._continuous_profile is not None: - self._continuous_profile.stop() - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self) -> "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_log_representation(self) -> str: - return "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "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. - - :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 - - 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 = [] - has_gen_ai_span = False - if client.options.get("stream_gen_ai_spans", True): - for span in self._span_recorder.spans: - if span.timestamp is None: - continue - - if isinstance(span.op, str) and span.op.startswith("gen_ai."): - has_gen_ai_span = True - - finished_spans.append(span.to_json()) - else: - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - len_diff = len(self._span_recorder.spans) - len(finished_spans) - dropped_spans = len_diff + self._span_recorder.dropped_spans - - # 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: "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, - } - - if dropped_spans > 0: - event["_dropped_spans"] = dropped_spans - - if has_gen_ai_span: - event["_has_gen_ai_span"] = True - - return scope.capture_event(event) - - def set_context(self, key: str, value: "dict[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: 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) -> "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) -> "Any": - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self) -> "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: "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 = self._get_log_representation() - - # 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 - if callable(client.options.get("traces_sampler")): - try: - sample_rate = client.options["traces_sampler"](sampling_context) - except Exception: - logger.warning( - "[Tracing] traces_sampler raised; falling back to parent sample rate or traces_sample_rate", - exc_info=True, - ) - sample_rate = ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - else: - sample_rate = ( - 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, or is using a fallback sample rate that is 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. - self.sampled = self._sample_rand < 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, - ) - ) - - # Private aliases matching StreamedSpan's private API - _get_baggage = get_baggage - _get_trace_context = get_trace_context - - -class NoOpSpan(Span): - def __repr__(self) -> str: - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self) -> "Optional[Transaction]": - return None - - def start_child(self, **kwargs: "Any") -> "NoOpSpan": - return NoOpSpan() - - def to_traceparent(self) -> str: - return "" - - def to_baggage(self) -> "Optional[Baggage]": - return None - - def get_baggage(self) -> "Optional[Baggage]": - return None - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - return iter(()) - - def set_tag(self, key: str, value: "Any") -> None: - pass - - def set_data(self, key: str, value: "Any") -> None: - pass - - def update_data(self, data: "Dict[str, Any]") -> None: - pass - - def set_status(self, value: str) -> None: - pass - - def set_http_status(self, http_status: int) -> None: - pass - - def is_success(self) -> bool: - return True - - def to_json(self) -> "Dict[str, Any]": - return {} - - def get_trace_context(self) -> "Any": - return {} - - def get_profile_context(self) -> "Any": - return {} - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - pass - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - pass - - def init_span_recorder(self, maxlen: int) -> None: - pass - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - pass - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _get_baggage = get_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -if TYPE_CHECKING: - - @overload - def trace( - func: None = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": - # Handles: @trace() and @trace(op="custom") - pass - - @overload - def trace(func: "Callable[P, R]") -> "Callable[P, R]": - # Handles: @trace - pass - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a child span around a function call. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, a default operation will - be assigned based on the template. - :type op: str or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - op=OP.DB_QUERY, - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import create_span_decorator - - decorator = create_span_decorator( - op=op, - name=name, - attributes=attributes, - ) - - if func: - return decorator(func) - else: - return decorator - - # Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - _generate_sample_rand, - extract_sentrytrace_data, - has_span_streaming_enabled, - has_tracing_enabled, -) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 63938bb1cd..756f934dc3 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -22,7 +22,6 @@ _is_in_project_root, _module_in_list, capture_internal_exceptions, - deprecation_warning, filename_for_module, has_data_collection_enabled, is_sentry_url, @@ -112,20 +111,6 @@ def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: ) -def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return False - - is_enabled_in_experiment_config = (options.get("_experiments") or {}).get( - "trace_lifecycle" - ) == "stream" - - if options.get("trace_lifecycle") is not None: - return options.get("trace_lifecycle") == "stream" - - return is_enabled_in_experiment_config - - @contextlib.contextmanager def record_sql_queries( cursor: "Any", @@ -180,7 +165,7 @@ def record_sql_queries( if query is not None: additional_attributes["db.query.text"] = query - with sentry_sdk.traces.start_span( + with sentry_sdk.start_span( name="" if query is None else query, attributes={ "sentry.origin": span_origin, @@ -381,9 +366,7 @@ def add_query_source( ) -def add_http_request_source( - span: "sentry_sdk.traces.StreamedSpan", -) -> None: +def add_http_request_source(span: "sentry_sdk.traces.StreamedSpan") -> None: """ Adds OTel compatible source code information to a span for an outgoing HTTP request """ @@ -776,56 +759,6 @@ def populate_from_propagation_context( return Baggage(sentry_items, third_party_items, mutable) - @classmethod - def populate_from_transaction( - cls, transaction: "sentry_sdk.tracing.Transaction" - ) -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - transaction.name - and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES - ): - sentry_items["transaction"] = transaction.name - - if transaction.sample_rate is not None: - sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction.sampled is not None: - sentry_items["sampled"] = "true" if transaction.sampled else "false" - - # there's an existing baggage but it was mutable, - # which is why we are creating this new baggage. - # However, if by chance the user put some sentry items in there, give them precedence. - if transaction._baggage and transaction._baggage.sentry_items: - sentry_items.update(transaction._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - @classmethod def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": """ @@ -990,109 +923,6 @@ def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]" def create_span_decorator( - op: "Optional[Union[str, OP]]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> "Any": - """ - Create a span decorator that can wrap both sync and async functions. - - :param op: The operation type for the span. - :type op: str or :py:class:`sentry_sdk.consts.OP` or None - :param name: The name of the span. - :type name: str or None - :param attributes: Additional attributes to set on the span. - :type attributes: dict or None - """ - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return await f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - deprecation_warning( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - ) - return await f(*args, **kwargs) - - span_op = op or OP.FUNCTION - function_name = name or qualname_from_function(f) or "" - span_name = function_name - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - - result = await f(*args, **kwargs) - - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - deprecation_warning( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - ) - return f(*args, **kwargs) - - span_op = op or OP.FUNCTION - function_name = name or qualname_from_function(f) or "" - span_name = function_name - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - - result = f(*args, **kwargs) - - return result - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def create_streaming_span_decorator( name: "Optional[str]" = None, attributes: "Optional[dict[str, Any]]" = None, active: bool = True, @@ -1111,16 +941,9 @@ def span_decorator(f: "Any") -> "Any": @functools.wraps(f) async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - logger.warning( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - ) - span_name = name or qualname_from_function(f) or "" - with start_streaming_span( + with sentry_sdk.start_span( name=span_name, attributes=new_attributes, active=active ): result = await f(*args, **kwargs) @@ -1133,16 +956,9 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": @functools.wraps(f) def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - logger.warning( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - ) - span_name = name or qualname_from_function(f) or "" - with start_streaming_span( + with sentry_sdk.start_span( name=span_name, attributes=new_attributes, active=active ): return f(*args, **kwargs) @@ -1160,17 +976,6 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": return span_decorator -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - def _generate_sample_rand( trace_id: "Optional[str]", *, @@ -1494,14 +1299,7 @@ def _matches(rule: "Any", value: "Any") -> bool: LOW_QUALITY_SEGMENT_SOURCES, StreamedSpan, ) -from sentry_sdk.traces import ( - start_span as start_streaming_span, -) from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, - LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 83af5c9509..42b795f681 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -36,7 +36,7 @@ except ImportError: ASYNC_TRANSPORT_AVAILABLE = False -from typing import TYPE_CHECKING, Dict, List, cast +from typing import TYPE_CHECKING, Dict, List import certifi import urllib3 @@ -159,13 +159,6 @@ def record_lost_event( If an item is provided, the data category and quantity are extracted from the item, and the values passed for data_category and quantity are ignored. - - When recording a lost transaction via data_category="transaction", - the calling code should also record the lost spans via this method. - When recording lost spans, `quantity` should be set to the number - of contained spans, plus one for the transaction itself. When - passing an Item containing a transaction via the `item` parameter, - this method automatically records the lost spans. """ return None @@ -271,17 +264,7 @@ def record_lost_event( data_category = item.data_category quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). - if data_category == "transaction": - # Also record the lost spans - event = item.get_transaction_event() or {} - - # +1 for the transaction itself - span_count = ( - len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 - ) - self.record_lost_event(reason, "span", quantity=span_count) - - elif data_category == "log_item" and item: + if data_category == "log_item" and item: # Also record size of lost logs in bytes bytes_size = len(item.get_bytes()) self.record_lost_event(reason, "log_byte", quantity=bytes_size) @@ -453,7 +436,7 @@ def _prepare_envelope( new_items = [] for item in envelope.items: if self._check_disabled(item.data_category): - if item.data_category in ("transaction", "error", "default", "statsd"): + if item.data_category in ("error", "default", "statsd"): self.on_dropped_event("self_rate_limits") self.record_lost_event("ratelimit_backoff", item=item) else: diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 0381af78ba..c500f55574 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1807,7 +1807,7 @@ def ensure_integration_enabled( ```python @ensure_integration_enabled(MyIntegration, my_function) def patch_my_function(): - with sentry_sdk.start_transaction(...): + with sentry_sdk.traces.start_span(...): return my_function() ``` """ diff --git a/tests/test_api.py b/tests/test_api.py index e4d3569172..ce57b2d224 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -21,10 +21,10 @@ def test_get_current_span(): fake_scope = mock.MagicMock() - fake_scope.span = mock.MagicMock() - assert get_current_span(fake_scope) == fake_scope.span + fake_scope.streamed_span = mock.MagicMock() + assert get_current_span(fake_scope) == fake_scope.streamed_span - fake_scope.span = None + fake_scope.streamed_span = None assert get_current_span(fake_scope) is None diff --git a/tests/test_client.py b/tests/test_client.py index 73b1d9fdc6..624fc7f713 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,7 +4,6 @@ import subprocess import sys import time -from collections import Counter, defaultdict from collections.abc import Mapping from textwrap import dedent from unittest import mock @@ -1277,104 +1276,6 @@ def test_uwsgi_warnings(sentry_init, opt, missing_flags): mock_logger.warning.assert_not_called() -class TestSpanClientReports: - """ - Tests for client reports related to spans. - """ - - __test__ = False - - @staticmethod - def span_dropper(spans_to_drop): - """ - Returns a function that can be used to drop spans from an event. - """ - - def drop_spans(event, _): - event["spans"] = event["spans"][spans_to_drop:] - return event - - return drop_spans - - @staticmethod - def mock_transaction_event(span_count): - """ - Returns a mock transaction event with the given number of spans. - """ - - return defaultdict( - mock.MagicMock, - type="transaction", - spans=[mock.MagicMock() for _ in range(span_count)], - ) - - def __init__(self, span_count): - """Configures a test case with the number of spans dropped and whether the transaction was dropped.""" - self.span_count = span_count - self.expected_record_lost_event_calls = Counter() - self.before_send = lambda event, _: event - self.event_processor = lambda event, _: event - - def _update_resulting_calls(self, reason, drops_transactions=0, drops_spans=0): - """ - Updates the expected calls with the given resulting calls. - """ - if drops_transactions > 0: - self.expected_record_lost_event_calls[ - (reason, "transaction", None, drops_transactions) - ] += 1 - - if drops_spans > 0: - self.expected_record_lost_event_calls[ - (reason, "span", None, drops_spans) - ] += 1 - - def with_before_send( - self, - before_send, - *, - drops_transactions=0, - drops_spans=0, - ): - self.before_send = before_send - self._update_resulting_calls( - "before_send", - drops_transactions, - drops_spans, - ) - - return self - - def with_event_processor( - self, - event_processor, - *, - drops_transactions=0, - drops_spans=0, - ): - self.event_processor = event_processor - self._update_resulting_calls( - "event_processor", - drops_transactions, - drops_spans, - ) - - return self - - def run(self, sentry_init, capture_record_lost_event_calls): - """Runs the test case with the configured parameters.""" - sentry_init(before_send_transaction=self.before_send) - record_lost_event_calls = capture_record_lost_event_calls() - - with sentry_sdk.isolation_scope() as scope: - scope.add_event_processor(self.event_processor) - event = self.mock_transaction_event(self.span_count) - sentry_sdk.get_client().capture_event(event, scope=scope) - - # We use counters to ensure that the calls are made the expected number of times, disregarding order. - assert Counter(record_lost_event_calls) == self.expected_record_lost_event_calls - - def make_options_transport_cls(): """Make an options transport class that captures the options passed to it.""" # We need a unique class for each test so that the options are not diff --git a/tests/test_transport.py b/tests/test_transport.py index f29d6a4b65..e02473875d 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -4,7 +4,6 @@ import pickle import socket import sys -from collections import defaultdict from datetime import datetime, timedelta, timezone from unittest import mock @@ -94,19 +93,6 @@ def inner(**kwargs): return inner -def mock_transaction_envelope(span_count: int) -> "Envelope": - event = defaultdict( - mock.MagicMock, - type="transaction", - spans=[mock.MagicMock() for _ in range(span_count)], - ) - - envelope = Envelope() - envelope.add_transaction(event) - - return envelope - - # The compression-relevant dimensions (level x algo x http2) are fully # crossed; debug, flush method and pickling are rotated through the cases # so every value of every dimension is still exercised.