diff --git a/docs/conf.py b/docs/conf.py index 1d58274beb..64d49a1188 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,8 @@ import sphinx.ext.autodoc # noqa: F401 import sphinx.ext.intersphinx # noqa: F401 import urllib3.exceptions # noqa: F401 +import importlib_metadata # noqa: F401 +import opentelemetry.sdk.metrics._internal # noqa: F401 typing.TYPE_CHECKING = True diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 86577cc500..50c12dd636 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -7,6 +7,7 @@ from sentry_sdk.crons import monitor # TODO-neel-potel make 2 scope strategies/impls and switch +from sentry_sdk.scope import Scope as BaseScope from sentry_sdk.integrations.opentelemetry.scope import ( PotelScope as Scope, new_scope, @@ -123,7 +124,7 @@ def is_initialized(): @scopemethod def get_global_scope(): - # type: () -> Scope + # type: () -> BaseScope return Scope.get_global_scope() @@ -239,7 +240,7 @@ def flush( def start_span(**kwargs): - # type: (type.Any) -> Span + # type: (Any) -> Span """ Start and return a span. diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 85623d8056..fefdcaecd8 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -483,7 +483,7 @@ def _prepare_event( for key in "release", "environment", "server_name", "dist": if event.get(key) is None and self.options[key] is not None: - event[key] = str(self.options[key]).strip() # type: ignore[literal-required] + event[key] = str(self.options[key]).strip() if event.get("sdk") is None: sdk_info = dict(SDK_INFO) sdk_info["integrations"] = sorted(self.integrations.keys()) diff --git a/sentry_sdk/integrations/opentelemetry/contextvars_context.py b/sentry_sdk/integrations/opentelemetry/contextvars_context.py index 8025f26ba8..df818bc399 100644 --- a/sentry_sdk/integrations/opentelemetry/contextvars_context.py +++ b/sentry_sdk/integrations/opentelemetry/contextvars_context.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from typing import Optional - from sentry_sdk.integrations.opentelemetry.scope import PotelScope + import sentry_sdk.integrations.opentelemetry.scope as scope class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext): @@ -29,16 +29,16 @@ def attach(self, context): should_use_isolation_scope = context.pop(SENTRY_USE_ISOLATION_SCOPE_KEY, None) should_use_isolation_scope = cast( - "Optional[PotelScope]", should_use_isolation_scope + "Optional[scope.PotelScope]", should_use_isolation_scope ) should_use_current_scope = context.pop(SENTRY_USE_CURRENT_SCOPE_KEY, None) should_use_current_scope = cast( - "Optional[PotelScope]", should_use_current_scope + "Optional[scope.PotelScope]", should_use_current_scope ) if scopes: - scopes = cast("tuple[PotelScope, PotelScope]", scopes) + scopes = cast("tuple[scope.PotelScope, scope.PotelScope]", scopes) (current_scope, isolation_scope) = scopes else: current_scope = sentry_sdk.get_current_scope() diff --git a/sentry_sdk/integrations/opentelemetry/propagator.py b/sentry_sdk/integrations/opentelemetry/propagator.py index 37d6362f82..fcc2009849 100644 --- a/sentry_sdk/integrations/opentelemetry/propagator.py +++ b/sentry_sdk/integrations/opentelemetry/propagator.py @@ -36,7 +36,7 @@ if TYPE_CHECKING: from typing import Optional, Set - from sentry_sdk.integrations.opentelemetry.scope import PotelScope + import sentry_sdk.integrations.opentelemetry.scope as scope class SentryPropagator(TextMapPropagator): @@ -94,7 +94,7 @@ def inject(self, carrier, context=None, setter=default_setter): scopes = get_value(SENTRY_SCOPES_KEY, context) if scopes: - scopes = cast("tuple[PotelScope, PotelScope]", scopes) + scopes = cast("tuple[scope.PotelScope, scope.PotelScope]", scopes) (current_scope, _) = scopes # TODO-neel-potel check trace_propagation_targets diff --git a/sentry_sdk/integrations/opentelemetry/scope.py b/sentry_sdk/integrations/opentelemetry/scope.py index d16215ab20..c60e5eb716 100644 --- a/sentry_sdk/integrations/opentelemetry/scope.py +++ b/sentry_sdk/integrations/opentelemetry/scope.py @@ -29,6 +29,7 @@ from sentry_sdk.integrations.opentelemetry.utils import trace_state_from_baggage from sentry_sdk.scope import Scope, ScopeType from sentry_sdk.tracing import Span +from sentry_sdk.utils import logger from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: @@ -41,15 +42,17 @@ class PotelScope(Scope): @classmethod def _get_scopes(cls): - # type: () -> Optional[Tuple[Scope, Scope]] + # type: () -> Optional[Tuple[PotelScope, PotelScope]] """ Returns the current scopes tuple on the otel context. Internal use only. """ - return cast("Optional[Tuple[Scope, Scope]]", get_value(SENTRY_SCOPES_KEY)) + return cast( + "Optional[Tuple[PotelScope, PotelScope]]", get_value(SENTRY_SCOPES_KEY) + ) @classmethod def get_current_scope(cls): - # type: () -> Scope + # type: () -> PotelScope """ Returns the current scope. """ @@ -57,7 +60,7 @@ def get_current_scope(cls): @classmethod def _get_current_scope(cls): - # type: () -> Optional[Scope] + # type: () -> Optional[PotelScope] """ Returns the current scope without creating a new one. Internal use only. """ @@ -66,7 +69,7 @@ def _get_current_scope(cls): @classmethod def get_isolation_scope(cls): - # type: () -> Scope + # type: () -> PotelScope """ Returns the isolation scope. """ @@ -74,7 +77,7 @@ def get_isolation_scope(cls): @classmethod def _get_isolation_scope(cls): - # type: () -> Optional[Scope] + # type: () -> Optional[PotelScope] """ Returns the isolation scope without creating a new one. Internal use only. """ @@ -84,6 +87,11 @@ def _get_isolation_scope(cls): @contextmanager def continue_trace(self, environ_or_headers): # type: (Dict[str, Any]) -> Generator[None, None, None] + """ + Sets the propagation context from environment or headers to continue an incoming trace. + Any span started within this context manager will use the same trace_id, parent_span_id + and inherit the sampling decision from the incoming trace. + """ self.generate_propagation_context(environ_or_headers) span_context = self._incoming_otel_span_context() @@ -118,8 +126,8 @@ def _incoming_otel_span_context(self): trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "deferred") span_context = SpanContext( - trace_id=int(self._propagation_context.trace_id, 16), # type: ignore - span_id=int(self._propagation_context.parent_span_id, 16), # type: ignore + trace_id=int(self._propagation_context.trace_id, 16), + span_id=int(self._propagation_context.parent_span_id, 16), is_remote=True, trace_flags=trace_flags, trace_state=trace_state, @@ -134,18 +142,22 @@ def start_transaction(self, **kwargs): This function is deprecated and will be removed in a future release. Use :py:meth:`sentry_sdk.start_span` instead. """ + logger.warning( + "The `start_transaction` method is deprecated, please use `sentry_sdk.start_span instead.`" + ) return self.start_span(**kwargs) def start_span(self, **kwargs): # type: (Any) -> Span - return Span(**kwargs, scope=self) + return Span(**kwargs) -_INITIAL_CURRENT_SCOPE = None -_INITIAL_ISOLATION_SCOPE = None +_INITIAL_CURRENT_SCOPE = PotelScope(ty=ScopeType.CURRENT) +_INITIAL_ISOLATION_SCOPE = PotelScope(ty=ScopeType.ISOLATION) def setup_initial_scopes(): + # type: () -> None global _INITIAL_CURRENT_SCOPE, _INITIAL_ISOLATION_SCOPE _INITIAL_CURRENT_SCOPE = PotelScope(ty=ScopeType.CURRENT) _INITIAL_ISOLATION_SCOPE = PotelScope(ty=ScopeType.ISOLATION) diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index d890849c31..34fea2f46d 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -95,7 +95,7 @@ def convert_from_otel_timestamp(time): def convert_to_otel_timestamp(time): - # type: (Union[datetime.datetime, float]) -> int + # type: (Union[datetime, float]) -> int """Convert a datetime to an OTel timestamp (with nanosecond precision).""" if isinstance(time, datetime): return int(time.timestamp() * 1e9) @@ -121,9 +121,12 @@ def extract_span_data(span): if span.attributes is None: return (op, description, status, http_status, origin) - op = span.attributes.get(SentrySpanAttribute.OP) or op - description = span.attributes.get(SentrySpanAttribute.DESCRIPTION) or description - origin = span.attributes.get(SentrySpanAttribute.ORIGIN) + attribute_op = cast("Optional[str]", span.attributes.get(SentrySpanAttribute.OP)) + op = attribute_op or op + description = cast( + "str", span.attributes.get(SentrySpanAttribute.DESCRIPTION) or description + ) + origin = cast("Optional[str]", span.attributes.get(SentrySpanAttribute.ORIGIN)) http_method = span.attributes.get(SpanAttributes.HTTP_METHOD) http_method = cast("Optional[str]", http_method) @@ -137,7 +140,7 @@ def extract_span_data(span): rpc_service = span.attributes.get(SpanAttributes.RPC_SERVICE) if rpc_service: return ( - span.attributes.get(SentrySpanAttribute.OP) or "rpc", + attribute_op or "rpc", description, status, http_status, @@ -147,7 +150,7 @@ def extract_span_data(span): messaging_system = span.attributes.get(SpanAttributes.MESSAGING_SYSTEM) if messaging_system: return ( - span.attributes.get(SentrySpanAttribute.OP) or "message", + attribute_op or "message", description, status, http_status, @@ -165,7 +168,7 @@ def span_data_for_http_method(span): # type: (ReadableSpan) -> OtelExtractedSpanData span_attributes = span.attributes or {} - op = span_attributes.get(SentrySpanAttribute.OP) + op = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.OP)) if op is None: op = "http" @@ -183,6 +186,7 @@ def span_data_for_http_method(span): description = span_attributes.get( SentrySpanAttribute.DESCRIPTION ) or span_attributes.get(SentrySpanAttribute.NAME) + description = cast("Optional[str]", description) if description is None: description = f"{http_method}" @@ -205,7 +209,7 @@ def span_data_for_http_method(span): status, http_status = extract_span_status(span) - origin = span_attributes.get(SentrySpanAttribute.ORIGIN) + origin = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.ORIGIN)) return (op, description, status, http_status, origin) @@ -214,13 +218,13 @@ def span_data_for_db_query(span): # type: (ReadableSpan) -> OtelExtractedSpanData span_attributes = span.attributes or {} - op = span_attributes.get(SentrySpanAttribute.OP, OP.DB) + op = cast("str", span_attributes.get(SentrySpanAttribute.OP, OP.DB)) statement = span_attributes.get(SpanAttributes.DB_STATEMENT, None) statement = cast("Optional[str]", statement) description = statement or span.name - origin = span_attributes.get(SentrySpanAttribute.ORIGIN) + origin = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.ORIGIN)) return (op, description, None, None, origin) @@ -293,19 +297,20 @@ def extract_span_attributes(span, namespace): """ Extract Sentry-specific span attributes and make them look the way Sentry expects. """ - extracted_attrs = {} + extracted_attrs = {} # type: dict[str, Any] for attr, value in (span.attributes or {}).items(): if attr.startswith(namespace): key = attr[len(namespace) + 1 :] if namespace == SentrySpanAttribute.MEASUREMENT: - value = { + value = cast("tuple[str, str]", value) + extracted_attrs[key] = { "value": float(value[0]), "unit": value[1], } - - extracted_attrs[key] = value + else: + extracted_attrs[key] = value return extracted_attrs @@ -457,7 +462,7 @@ def set_sentry_meta(span, key, value): # type: (Union[AbstractSpan, ReadableSpan], str, Any) -> None sentry_meta = getattr(span, "_sentry_meta", {}) sentry_meta[key] = value - span._sentry_meta = sentry_meta + span._sentry_meta = sentry_meta # type: ignore[union-attr] def get_profile_context(span): diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index af69aca4ee..45f3dfe0a0 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -12,13 +12,11 @@ from sentry_sdk.attachments import Attachment from sentry_sdk.consts import DEFAULT_MAX_BREADCRUMBS, FALSE_VALUES from sentry_sdk.feature_flags import FlagBuffer, DEFAULT_FLAG_CAPACITY -from sentry_sdk.profiler.continuous_profiler import try_autostart_continuous_profiler from sentry_sdk.profiler.transaction_profiler import Profile from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( Baggage, has_tracing_enabled, - normalize_incoming_data, PropagationContext, ) from sentry_sdk.tracing import ( @@ -55,6 +53,7 @@ from typing import Tuple from typing import TypeVar from typing import Union + from typing import Self from typing_extensions import Unpack @@ -67,7 +66,6 @@ ExcInfo, Hint, LogLevelStr, - SamplingContext, Type, ) @@ -194,12 +192,12 @@ def __init__(self, ty=None, client=None): self.generate_propagation_context(incoming_data=incoming_trace_information) def __copy__(self): - # type: () -> Scope + # type: () -> Self """ Returns a copy of this scope. This also creates a copy of all referenced data structures. """ - rv = object.__new__(self.__class__) # type: Scope + rv = object.__new__(self.__class__) # type: Self rv._type = self._type rv.client = self.client @@ -333,7 +331,7 @@ def last_event_id(cls): return cls.get_isolation_scope()._last_event_id def _merge_scopes(self, additional_scope=None, additional_scope_kwargs=None): - # type: (Optional[Scope], Optional[Dict[str, Any]]) -> Scope + # type: (Optional[Scope], Optional[Dict[str, Any]]) -> Self """ Merges global, isolation and current scope into a new scope and adds the given additional scope or additional scope kwargs to it. @@ -419,7 +417,7 @@ def set_client(self, client=None): self.client = client if client is not None else NonRecordingClient() def fork(self): - # type: () -> Scope + # type: () -> Self """ .. versionadded:: 2.0.0 @@ -949,71 +947,20 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): while len(self._breadcrumbs) > max_breadcrumbs: self._breadcrumbs.popleft() - def start_transaction(self, transaction=None, **kwargs): - # type: (Optional[Span], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Span, NoOpSpan] + def start_transaction(self, **kwargs): + # type: (Unpack[TransactionKwargs]) -> Union[NoOpSpan, Span] """ - 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 kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. + Use :py:meth:`sentry_sdk.start_span` instead. """ - # TODO-neel-potel fix signature and no op - kwargs.setdefault("scope", self) - - client = self.get_client() - - try_autostart_continuous_profiler() - - # if we haven't been given a transaction, make one - transaction = 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, - } - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - - # 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 + logger.warning( + "The `start_transaction` method is deprecated, please use `sentry_sdk.start_span instead.`" + ) + return NoOpSpan(**kwargs) def start_span(self, **kwargs): - # type: (Optional[Span], Any) -> Span + # type: (Any) -> Union[NoOpSpan, Span] """ Start a span whose parent is the currently active span, if any. @@ -1023,53 +970,16 @@ def start_span(self, **kwargs): For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. """ - # TODO-neel-potel fix signature and no op - if kwargs.get("description") is not None: - warnings.warn( - "The `description` parameter is deprecated. Please use `name` instead.", - DeprecationWarning, - stacklevel=2, - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - # get current span or transaction - span = self.span or self.get_isolation_scope().span + return NoOpSpan(**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 - - def continue_trace( - self, environ_or_headers, op=None, name=None, source=None, origin=None - ): - # TODO-neel-potel fix signature and no op - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], Optional[str]) -> Span + @contextmanager + def continue_trace(self, environ_or_headers): + # type: (Dict[str, Any]) -> Generator[None, None, None] """ - Sets the propagation context from environment or headers and returns a transaction. + Sets the propagation context from environment or headers to continue an incoming trace. """ self.generate_propagation_context(environ_or_headers) - - transaction = Span.continue_from_headers( - normalize_incoming_data(environ_or_headers), - op=op, - origin=origin, - name=name, - source=source, - ) - - return transaction + yield def capture_event(self, event, hint=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str] @@ -1382,7 +1292,7 @@ def run_event_processors(self, event, hint): ) for event_processor in event_processors: - new_event = event + new_event = event # type: Optional[Event] with capture_internal_exceptions(): new_event = event_processor(event, hint) if new_event is None: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 7b534d7efd..9d0ee6404d 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -200,6 +200,10 @@ def get_span_status_from_http_code(http_status_code): class NoOpSpan: + def __init__(self, **kwargs): + # type: (Any) -> None + pass + def __repr__(self): # type: () -> str return "<%s>" % self.__class__.__name__ @@ -263,7 +267,6 @@ def get_profile_context(self): def finish( self, - scope=None, # type: Optional[sentry_sdk.Scope] end_timestamp=None, # type: Optional[Union[float, datetime]] ): # type: (...) -> None