diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index ebec367f0b..1de39fcadc 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -41,18 +41,10 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-loguru" - - name: Test opentelemetry - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-opentelemetry" - name: Test otlp run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-otlp" - - name: Test potel - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-potel" - name: Test pure_eval run: | set -x # print commands that are executed diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 53396a37ba..6809604e6e 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,3 +1,40 @@ +# Sentry SDK 3.0 Migration Guide + + +Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of what's changed. Looking for a more digestable summary? See the [guide in the docs](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x) with the most common migration patterns. + +## New Features + + +## Changed + +- The Strawberry integration won't auto-enable anymore if we detect `strawberry-graphql` is installed. Set it up manually, setting the `async_execution` integration option to either `True` or `False` depending on if your app is async or sync. + + ```python + from sentry_sdk.integrations.strawberry import StrawberryIntegration + + sentry_sdk.init( + integrations=[ + StrawberryIntegration(async_execution=True), # or False + ], + ... + ) + ``` + +- The UnraisableHookIntegration is now enabled by default. +- We now don't suppress chained exceptions in the ASGI and asyncio integrations by default. The related `suppress_asgi_chained_exceptions` experimental option was removed. + +## Removed + +- Transaction profiling and related code was removed. +- Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. +- The `SentrySpanProcessor`, `SentryPropagator`, `instrumenter`, and associated OpenTelemetry compatibility code was removed along with the `opentelemetry` extra and the `SentryPropagator` entrypoint. Use the `OTLPIntegration` instead. +- Removed the `auto_session_tracing` decorator. Use `track_session` instead. +- The experimental option `otel_powered_performance` has been removed together with the associated `OpenTelemetryIntegration` and `opentelemetry-experimental` extra. + +## Deprecated + + # Sentry SDK 2.0 Migration Guide Looking to upgrade from Sentry SDK 1.x to 2.x? Here's a comprehensive list of what's changed. Looking for a more digestable summary? See the [guide in the docs](https://docs.sentry.io/platforms/python/migration/1.x-to-2.x) with the most common migration patterns. diff --git a/docs/apidocs.rst b/docs/apidocs.rst index a3c8a6e150..4991b8ccd6 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -2,9 +2,6 @@ API Docs ======== -.. autoclass:: sentry_sdk.Hub - :members: - .. autoclass:: sentry_sdk.Scope :members: @@ -32,9 +29,6 @@ API Docs .. autoclass:: sentry_sdk.tracing.Span :members: -.. autoclass:: sentry_sdk.profiler.transaction_profiler.Profile - :members: - .. autoclass:: sentry_sdk.session.Session :members: diff --git a/scripts/find_raise_from_none.py b/scripts/find_raise_from_none.py index 63b2b84333..089a14a1df 100644 --- a/scripts/find_raise_from_none.py +++ b/scripts/find_raise_from_none.py @@ -42,17 +42,11 @@ def main(): for module_path in walk_package_modules(): scan_file(module_path) - # TODO: Investigate why we suppress exception chains here. - ignored_raises = { - pathlib.Path("sentry_sdk/integrations/asgi.py"): 2, - pathlib.Path("sentry_sdk/integrations/asyncio.py"): 1, - } - raise_from_none_count = { file: len(occurences) for file, occurences in RaiseFromNoneVisitor.line_numbers.items() } - if raise_from_none_count != ignored_raises: + if raise_from_none_count: exc = Exception("Detected unexpected raise ... from None.") exc.add_note( "Raise ... from None suppresses chained exceptions, removing valuable context." diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index a8609dab50..1fa9e765c5 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -69,9 +69,7 @@ "shadowed_module", "gcp", "gevent", - "opentelemetry", "otlp", - "potel", } # Free-threading is experimentally supported in 3.13, and officially supported in 3.14. diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 12a2038683..459fc00cfa 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -42,15 +42,9 @@ envlist = # GCP {py3.7}-gcp - # OpenTelemetry (OTel) - {py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-opentelemetry - # OpenTelemetry with OTLP {py3.7,py3.9,py3.12,py3.13,py3.14}-otlp - # OpenTelemetry Experimental (POTel) - {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -139,18 +133,10 @@ deps = aws_lambda: uvicorn aws_lambda: pyyaml - # OpenTelemetry (OTel) - opentelemetry: opentelemetry-distro - opentelemetry: pytest-forked - # OpenTelemetry with OTLP otlp: opentelemetry-distro[otlp] otlp: responses - # OpenTelemetry Experimental (POTel) - potel: opentelemetry-distro - potel: pytest-forked - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -240,9 +226,7 @@ setenv = aws_lambda: _TESTPATH=tests/integrations/aws_lambda cloud_resource_context: _TESTPATH=tests/integrations/cloud_resource_context gcp: _TESTPATH=tests/integrations/gcp - opentelemetry: _TESTPATH=tests/integrations/opentelemetry otlp: _TESTPATH=tests/integrations/otlp - potel: _TESTPATH=tests/integrations/opentelemetry socket: _TESTPATH=tests/integrations/socket # These TESTPATH definitions are auto-generated by toxgen diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index d6b5494515..754315d72c 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -163,9 +163,7 @@ ], "Misc": [ "loguru", - "opentelemetry", "otlp", - "potel", "pure_eval", "trytond", "typer", diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 9163a06641..e47021ee1f 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -8,7 +8,6 @@ from sentry_sdk.api import * # noqa # isort: skip __all__ = [ # noqa - "Hub", "Scope", "Client", "Transport", @@ -66,6 +65,3 @@ init_debug_support() del init_debug_support - -# circular imports -from sentry_sdk.hub import Hub diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index e91fed097c..182a364130 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -274,7 +274,6 @@ class DataCollection(TypedDict): "monitor_config": Mapping[str, object], "monitor_slug": Optional[str], "platform": Literal["python"], - "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports "release": Optional[str], "request": dict[str, object], "sdk": Mapping[str, object], @@ -418,7 +417,6 @@ class DataCollection(TypedDict): "attachment", "session", "internal", - "profile", "profile_chunk", "monitor", "span", @@ -429,7 +427,7 @@ class DataCollection(TypedDict): SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] ContinuousProfilerMode = Literal["thread", "gevent", "unknown"] - ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]] + ProfilerMode = Union[ContinuousProfilerMode] MonitorConfigScheduleType = Literal["crontab", "interval"] MonitorConfigScheduleUnit = Literal[ diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index d5f91fa145..4aa81e34ca 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -5,7 +5,6 @@ from sentry_sdk import Client, tracing_utils from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.crons import monitor from sentry_sdk.scope import Scope, _ScopeManager, isolation_scope, new_scope from sentry_sdk.traces import StreamedSpan @@ -383,7 +382,6 @@ def start_span( @scopemethod def start_transaction( transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, custom_sampling_context: "Optional[SamplingContext]" = None, **kwargs: "Unpack[TransactionKwargs]", ) -> "Union[Transaction, NoOpSpan]": @@ -411,15 +409,13 @@ def start_transaction( :param transaction: The transaction to start. If omitted, we create and start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. :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, instrumenter, custom_sampling_context, **kwargs + transaction, custom_sampling_context, **kwargs ) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e8414d3f91..fb315cd674 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -17,7 +17,6 @@ from sentry_sdk.consts import ( DEFAULT_MAX_VALUE_LENGTH, DEFAULT_OPTIONS, - INSTRUMENTER, SPANDATA, SPANSTATUS, VERSION, @@ -28,15 +27,10 @@ _resolve_data_collection, ) from sentry_sdk.envelope import Envelope, Item, PayloadRef -from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations +from sentry_sdk.integrations import setup_integrations from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.monitor import Monitor from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler -from sentry_sdk.profiler.transaction_profiler import ( - Profile, - has_profiling_enabled, - setup_profiler, -) from sentry_sdk.scrubber import EventScrubber from sentry_sdk.serializer import serialize from sentry_sdk.sessions import SessionFlusher @@ -336,9 +330,6 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": if rv["server_name"] is None and hasattr(socket, "gethostname"): rv["server_name"] = socket.gethostname() - if rv["instrumenter"] is None: - rv["instrumenter"] = INSTRUMENTER.SENTRY - if rv["project_root"] is None: try: project_root = os.getcwd() @@ -632,7 +623,6 @@ def _record_lost_event( self.options["send_default_pii"] = True self.options["error_sampler"] = sample_all self.options["traces_sampler"] = sample_all - self.options["profiles_sampler"] = sample_all # data_collection was resolved in _get_options() before this # spotlight override flipped send_default_pii on. Re-derive it so # data_collection agrees with should_send_default_pii() in @@ -681,19 +671,6 @@ def _record_lost_event( ) ) - if self.options["_experiments"].get("otel_powered_performance", False): - logger.debug( - "[OTel] Enabling experimental OTel-powered performance monitoring." - ) - self.options["instrumenter"] = INSTRUMENTER.OTEL - if ( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - not in _DEFAULT_INTEGRATIONS - ): - _DEFAULT_INTEGRATIONS.append( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration", - ) - self.integrations = setup_integrations( self.options["integrations"], with_defaults=self.options["default_integrations"], @@ -708,20 +685,14 @@ def _record_lost_event( SDK_INFO["name"] = sdk_name logger.debug("Setting SDK name to '%s'", sdk_name) - if has_profiling_enabled(self.options): - try: - setup_profiler(self.options) - except Exception as e: - logger.debug("Can not set up profiler. (%s)", e) - else: - try: - setup_continuous_profiler( - self.options, - sdk_info=SDK_INFO, - capture_func=_capture_envelope, - ) - except Exception as e: - logger.debug("Can not set up continuous profiler. (%s)", e) + try: + setup_continuous_profiler( + self.options, + sdk_info=SDK_INFO, + capture_func=_capture_envelope, + ) + except Exception as e: + logger.debug("Can not set up continuous profiler. (%s)", e) finally: _client_init_debug.set(old_debug) @@ -733,7 +704,6 @@ def _record_lost_event( or self.log_batcher or self.metrics_batcher or self.span_batcher - or has_profiling_enabled(self.options) or isinstance(self.transport, HttpTransportCore) ): # If we have anything on that could spawn a background thread, we @@ -1121,8 +1091,6 @@ def capture_event( if not self._should_capture(event, hint, scope): return None - profile = event.pop("profile", None) - event_id = event.get("event_id") if event_id is None: event["event_id"] = event_id = uuid.uuid4().hex @@ -1163,9 +1131,6 @@ def capture_event( envelope = Envelope(headers=headers) - if is_transaction and isinstance(profile, Profile): - envelope.add_profile(profile.to_json(event_opt, self.options)) - if is_transaction and not span_recorder_has_gen_ai_span: envelope.add_transaction(event_opt) elif is_transaction: diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index c6db94d780..c2500ca913 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -70,7 +70,6 @@ class CompressionAlgo(Enum): "record_sql_params": Optional[bool], "continuous_profiling_auto_start": Optional[bool], "continuous_profiling_mode": Optional[ContinuousProfilerMode], - "otel_powered_performance": Optional[bool], "transport_zlib_compression_level": Optional[int], "transport_compression_level": Optional[int], "transport_compression_algo": Optional[CompressionAlgo], @@ -86,7 +85,6 @@ class CompressionAlgo(Enum): "before_send_span": Optional[ Callable[[SpanJSON, Hint], Optional[SpanJSON]] ], - "suppress_asgi_chained_exceptions": Optional[bool], "data_collection": Optional[DataCollectionUserOptions], }, total=False, @@ -115,11 +113,6 @@ def __str__(self) -> str: return self.value -class INSTRUMENTER: - SENTRY = "sentry" - OTEL = "otel" - - class SPANNAME: DB_COMMIT = "COMMIT" DB_ROLLBACK = "ROLLBACK" @@ -1302,8 +1295,6 @@ def __init__( traces_sample_rate: "Optional[float]" = None, trace_lifecycle: "Optional[Literal['static', 'stream']]" = None, traces_sampler: "Optional[TracesSampler]" = None, - profiles_sample_rate: "Optional[float]" = None, - profiles_sampler: "Optional[TracesSampler]" = None, profiler_mode: "Optional[ProfilerMode]" = None, profile_lifecycle: 'Literal["manual", "trace"]' = "manual", profile_session_sample_rate: "Optional[float]" = None, @@ -1313,7 +1304,6 @@ def __init__( send_client_reports: bool = True, _experiments: "Experiments" = {}, # noqa: B006 proxy_headers: "Optional[Dict[str, str]]" = None, - instrumenter: "Optional[str]" = INSTRUMENTER.SENTRY, before_send_transaction: "Optional[TransactionProcessor]" = None, project_root: "Optional[str]" = None, enable_tracing: "Optional[bool]" = None, @@ -1706,16 +1696,6 @@ def __init__( Return a string for that repr value to be used or `None` to continue serializing how Sentry would have done it anyway. - :param profiles_sample_rate: A number between `0` and `1`, controlling the percentage chance a given sampled - transaction will be profiled. - - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. - - This is relative to the tracing sample rate - e.g. `0.5` means 50% of sampled transactions will be - profiled. - - :param profiles_sampler: - :param profiler_mode: :param profile_lifecycle: @@ -1730,7 +1710,6 @@ def __init__( :param spotlight: - :param instrumenter: :param enable_logs: Set `enable_logs` to True to enable the SDK to emit Sentry logs. Defaults to False. diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index d2d4aae31a..c4e3a4d42d 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -59,12 +59,6 @@ def add_transaction( ) -> None: self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - def add_profile( - self, - profile: "Any", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=profile), type="profile")) - def add_profile_chunk( self, profile_chunk: "Any", @@ -259,8 +253,6 @@ def data_category(self) -> "EventDataCategory": return "trace_metric" elif ty == "client_report": return "internal" - elif ty == "profile": - return "profile" elif ty == "profile_chunk": return "profile_chunk" elif ty == "check_in": diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py deleted file mode 100644 index b17444d06e..0000000000 --- a/sentry_sdk/hub.py +++ /dev/null @@ -1,741 +0,0 @@ -import warnings -from contextlib import contextmanager -from typing import TYPE_CHECKING - -from sentry_sdk import ( - get_client, - get_current_scope, - get_global_scope, - get_isolation_scope, -) -from sentry_sdk._compat import with_metaclass -from sentry_sdk.client import Client -from sentry_sdk.consts import INSTRUMENTER -from sentry_sdk.scope import _ScopeManager -from sentry_sdk.tracing import ( - NoOpSpan, - Span, - Transaction, -) -from sentry_sdk.utils import ( - ContextVar, - logger, -) - -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - overload, - ) - - from typing_extensions import Unpack - - from sentry_sdk._types import ( - Breadcrumb, - BreadcrumbHint, - Event, - ExcInfo, - Hint, - LogLevelStr, - SamplingContext, - ) - from sentry_sdk.client import BaseClient - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.tracing import TransactionKwargs - - T = TypeVar("T") - -else: - - def overload(x: "T") -> "T": - return x - - -class SentryHubDeprecationWarning(DeprecationWarning): - """ - A custom deprecation warning to inform users that the Hub is deprecated. - """ - - _MESSAGE = ( - "`sentry_sdk.Hub` is deprecated and will be removed in a future major release. " - "Please consult our 1.x to 2.x migration guide for details on how to migrate " - "`Hub` usage to the new API: " - "https://docs.sentry.io/platforms/python/migration/1.x-to-2.x" - ) - - def __init__(self, *_: object) -> None: - super().__init__(self._MESSAGE) - - -@contextmanager -def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": - """Utility function to suppress deprecation warnings for the Hub.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=SentryHubDeprecationWarning) - yield - - -_local = ContextVar("sentry_current_hub") - - -class HubMeta(type): - @property - def current(cls) -> "Hub": - """Returns the current instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - rv = _local.get(None) - if rv is None: - with _suppress_hub_deprecation_warning(): - # This will raise a deprecation warning; suppress it since we already warned above. - rv = Hub(GLOBAL_HUB) - _local.set(rv) - return rv - - @property - def main(cls) -> "Hub": - """Returns the main instance of the hub.""" - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - return GLOBAL_HUB - - -class Hub(with_metaclass(HubMeta)): # type: ignore - """ - .. deprecated:: 2.0.0 - The Hub is deprecated. Its functionality will be merged into :py:class:`sentry_sdk.scope.Scope`. - - The hub wraps the concurrency management of the SDK. Each thread has - its own hub but the hub might transfer with the flow of execution if - context vars are available. - - If the hub is used with a with statement it's temporarily activated. - """ - - _stack: "List[Tuple[Optional[Client], Scope]]" = None # type: ignore[assignment] - _scope: "Optional[Scope]" = None - - # Mypy doesn't pick up on the metaclass. - - if TYPE_CHECKING: - current: "Hub" = None # type: ignore[assignment] - main: "Optional[Hub]" = None - - def __init__( - self, - client_or_hub: "Optional[Union[Hub, Client]]" = None, - scope: "Optional[Any]" = None, - ) -> None: - warnings.warn(SentryHubDeprecationWarning(), stacklevel=2) - - current_scope = None - - if isinstance(client_or_hub, Hub): - client = get_client() - if scope is None: - # hub cloning is going on, we use a fork of the current/isolation scope for context manager - scope = get_isolation_scope().fork() - current_scope = get_current_scope().fork() - else: - client = client_or_hub - get_global_scope().set_client(client) - - if scope is None: # so there is no Hub cloning going on - # just the current isolation scope is used for context manager - scope = get_isolation_scope() - current_scope = get_current_scope() - - if current_scope is None: - # just the current current scope is used for context manager - current_scope = get_current_scope() - - self._stack = [(client, scope)] # type: ignore - self._last_event_id: "Optional[str]" = None - self._old_hubs: "List[Hub]" = [] - - self._old_current_scopes: "List[Scope]" = [] - self._old_isolation_scopes: "List[Scope]" = [] - self._current_scope: "Scope" = current_scope - self._scope: "Scope" = scope - - def __enter__(self) -> "Hub": - self._old_hubs.append(Hub.current) - _local.set(self) - - current_scope = get_current_scope() - self._old_current_scopes.append(current_scope) - scope._current_scope.set(self._current_scope) - - isolation_scope = get_isolation_scope() - self._old_isolation_scopes.append(isolation_scope) - scope._isolation_scope.set(self._scope) - - return self - - def __exit__( - self, - exc_type: "Optional[type]", - exc_value: "Optional[BaseException]", - tb: "Optional[Any]", - ) -> None: - old = self._old_hubs.pop() - _local.set(old) - - old_current_scope = self._old_current_scopes.pop() - scope._current_scope.set(old_current_scope) - - old_isolation_scope = self._old_isolation_scopes.pop() - scope._isolation_scope.set(old_isolation_scope) - - def run( - self, - callback: "Callable[[], T]", - ) -> "T": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Runs a callback in the context of the hub. Alternatively the - with statement can be used on the hub directly. - """ - with self: - return callback() - - def get_integration( - self, - name_or_class: "Union[str, Type[Integration]]", - ) -> "Any": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.get_integration` instead. - - Returns the integration for this hub by name or class. If there - is no client bound or the client does not have that integration - then `None` is returned. - - If the return value is not `None` the hub is guaranteed to have a - client attached. - """ - return get_client().get_integration(name_or_class) - - @property - def client(self) -> "Optional[BaseClient]": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Please use :py:func:`sentry_sdk.api.get_client` instead. - - Returns the current client on the hub. - """ - client = get_client() - - if not client.is_active(): - return None - - return client - - @property - def scope(self) -> "Scope": - """ - .. deprecated:: 2.0.0 - This property is deprecated and will be removed in a future release. - Returns the current scope on the hub. - """ - return get_isolation_scope() - - def last_event_id(self) -> "Optional[str]": - """ - Returns the last event ID. - - .. deprecated:: 1.40.5 - This function is deprecated and will be removed in a future release. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly. - """ - logger.warning( - "Deprecated: last_event_id is deprecated. This will be removed in the future. The functions `capture_event`, `capture_message`, and `capture_exception` return the event ID directly." - ) - return self._last_event_id - - def bind_client( - self, - new: "Optional[BaseClient]", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.set_client` instead. - - Binds a new client to the hub. - """ - get_global_scope().set_client(new) - - def capture_event( - self, - event: "Event", - hint: "Optional[Hint]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_event` instead. - - Captures an event. - - Alias of :py:meth:`sentry_sdk.Scope.capture_event`. - - :param event: A ready-made event that can be directly sent to Sentry. - - :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - """ - last_event_id = get_current_scope().capture_event( - event, hint, scope=scope, **scope_kwargs - ) - - is_transaction = event.get("type") == "transaction" - if last_event_id is not None and not is_transaction: - self._last_event_id = last_event_id - - return last_event_id - - def capture_message( - self, - message: str, - level: "Optional[LogLevelStr]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_message` instead. - - Captures a message. - - Alias of :py:meth:`sentry_sdk.Scope.capture_message`. - - :param message: The string to send as the message to Sentry. - - :param level: If no level is provided, the default level is `info`. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_message( - message, level=level, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def capture_exception( - self, - error: "Optional[Union[BaseException, ExcInfo]]" = None, - scope: "Optional[Scope]" = None, - **scope_kwargs: "Any", - ) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.capture_exception` instead. - - Captures an exception. - - Alias of :py:meth:`sentry_sdk.Scope.capture_exception`. - - :param error: An exception to capture. If `None`, `sys.exc_info()` will be used. - - :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :param scope_kwargs: Optional data to apply to event. - For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`. - The `scope` and `scope_kwargs` parameters are mutually exclusive. - - :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.client._Client.capture_event`). - """ - last_event_id = get_current_scope().capture_exception( - error, scope=scope, **scope_kwargs - ) - - if last_event_id is not None: - self._last_event_id = last_event_id - - return last_event_id - - def add_breadcrumb( - self, - crumb: "Optional[Breadcrumb]" = None, - hint: "Optional[BreadcrumbHint]" = None, - **kwargs: "Any", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.add_breadcrumb` instead. - - Adds a breadcrumb. - - :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. - - :param hint: An optional value that can be used by `before_breadcrumb` - to customize the breadcrumbs that are emitted. - """ - get_isolation_scope().add_breadcrumb(crumb, hint, **kwargs) - - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_span` instead. - - 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`. - """ - scope = get_current_scope() - return scope.start_span(instrumenter=instrumenter, **kwargs) - - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_transaction` instead. - - 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. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`. - """ - scope = get_current_scope() - - # For backwards compatibility, we allow passing the scope as the hub. - # We need a major release to make this nice. (if someone searches the code: deprecated) - # Type checking disabled for this line because deprecated keys are not allowed in the type signature. - kwargs["hub"] = scope # type: ignore - - return scope.start_transaction( - transaction, instrumenter, custom_sampling_context, **kwargs - ) - - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - ) -> "Transaction": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.continue_trace` instead. - - Sets the propagation context from environment or headers and returns a transaction. - """ - return get_isolation_scope().continue_trace( - environ_or_headers=environ_or_headers, op=op, name=name, source=source - ) - - @overload - def push_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def push_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def push_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pushes a new layer on the scope stack. - - :param callback: If provided, this method pushes a scope, calls - `callback`, and pops the scope again. - - :returns: If no `callback` is provided, a context manager that should - be used to pop the scope again. - """ - if callback is not None: - with self.push_scope() as scope: - callback(scope) - return None - - return _ScopeManager(self) - - def pop_scope_unsafe(self) -> "Tuple[Optional[Client], Scope]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Pops a scope layer from the stack. - - Try to use the context manager :py:meth:`push_scope` instead. - """ - rv = self._stack.pop() - assert self._stack, "stack must have at least one layer" - return rv - - @overload - def configure_scope( - self, - callback: "Optional[None]" = None, - ) -> "ContextManager[Scope]": - pass - - @overload - def configure_scope( # noqa: F811 - self, - callback: "Callable[[Scope], None]", - ) -> None: - pass - - def configure_scope( # noqa - self, - callback: "Optional[Callable[[Scope], None]]" = None, - continue_trace: bool = True, - ) -> "Optional[ContextManager[Scope]]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - - Reconfigures the scope. - - :param callback: If provided, call the callback with the current scope. - - :returns: If no callback is provided, returns a context manager that returns the scope. - """ - scope = get_isolation_scope() - - if continue_trace: - scope.generate_propagation_context() - - if callback is not None: - # TODO: used to return None when client is None. Check if this changes behavior. - callback(scope) - - return None - - @contextmanager - def inner() -> "Generator[Scope, None, None]": - yield scope - - return inner() - - def start_session( - self, - session_mode: str = "application", - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.start_session` instead. - - Starts a new session. - """ - get_isolation_scope().start_session( - session_mode=session_mode, - ) - - def end_session(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.end_session` instead. - - Ends the current session if there is one. - """ - get_isolation_scope().end_session() - - def stop_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.stop_auto_session_tracking` instead. - - Stops automatic session tracking. - - This temporarily session tracking for the current scope when called. - To resume session tracking call `resume_auto_session_tracking`. - """ - get_isolation_scope().stop_auto_session_tracking() - - def resume_auto_session_tracking(self) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.resume_auto_session_tracking` instead. - - Resumes automatic session tracking for the current scope if - disabled earlier. This requires that generally automatic session - tracking is enabled. - """ - get_isolation_scope().resume_auto_session_tracking() - - def flush( - self, - timeout: "Optional[float]" = None, - callback: "Optional[Callable[[int, float], None]]" = None, - ) -> None: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.client._Client.flush` instead. - - Alias for :py:meth:`sentry_sdk.client._Client.flush` - """ - return get_client().flush(timeout=timeout, callback=callback) - - def get_traceparent(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_traceparent` instead. - - Returns the traceparent either from the active span or from the scope. - """ - current_scope = get_current_scope() - traceparent = current_scope.get_traceparent() - - if traceparent is None: - isolation_scope = get_isolation_scope() - traceparent = isolation_scope.get_traceparent() - - return traceparent - - def get_baggage(self) -> "Optional[str]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.get_baggage` instead. - - Returns Baggage either from the active span or from the scope. - """ - current_scope = get_current_scope() - baggage = current_scope.get_baggage() - - if baggage is None: - isolation_scope = get_isolation_scope() - baggage = isolation_scope.get_baggage() - - if baggage is not None: - return baggage.serialize() - - return None - - def iter_trace_propagation_headers( - self, span: "Optional[Span]" = None - ) -> "Generator[Tuple[str, str], None, None]": - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.iter_trace_propagation_headers` instead. - - Return HTTP headers which allow propagation of trace data. Data taken - from the span representing the request, if available, or the current - span on the scope if not. - """ - return get_current_scope().iter_trace_propagation_headers( - span=span, - ) - - def trace_propagation_meta(self, span: "Optional[Span]" = None) -> str: - """ - .. deprecated:: 2.0.0 - This function is deprecated and will be removed in a future release. - Please use :py:meth:`sentry_sdk.Scope.trace_propagation_meta` instead. - - Return meta tags which should be injected into HTML templates - to allow propagation of trace information. - """ - if span is not None: - logger.warning( - "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future." - ) - - return get_current_scope().trace_propagation_meta( - span=span, - ) - - -with _suppress_hub_deprecation_warning(): - # Suppress deprecation warning for the Hub here, since we still always - # import this module. - GLOBAL_HUB = Hub() -_local.set(GLOBAL_HUB) - - -# Circular imports -from sentry_sdk import scope diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 677d34a81e..7aa496cec2 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -1,3 +1,4 @@ +import sys from abc import ABC, abstractmethod from threading import Lock from typing import TYPE_CHECKING @@ -64,6 +65,11 @@ def iter_default_integrations( "sentry_sdk.integrations.threading.ThreadingIntegration", ] +if sys.version_info >= (3, 8): + _DEFAULT_INTEGRATIONS.append( + "sentry_sdk.integrations.unraisablehook.UnraisablehookIntegration" + ) + _AUTO_ENABLING_INTEGRATIONS = [ "sentry_sdk.integrations.aiohttp.AioHttpIntegration", "sentry_sdk.integrations.anthropic.AnthropicIntegration", @@ -104,7 +110,6 @@ def iter_default_integrations( "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", "sentry_sdk.integrations.starlette.StarletteIntegration", "sentry_sdk.integrations.starlite.StarliteIntegration", - "sentry_sdk.integrations.strawberry.StrawberryIntegration", "sentry_sdk.integrations.tornado.TornadoIntegration", ] diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index e1157dda77..9921e6c1ff 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -205,15 +205,6 @@ async def _run_app( return await self.app(scope, receive, send) except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_lifespan_exception(exc) - raise exc from None - exc_info = sys.exc_info() with capture_internal_exceptions(): self._capture_lifespan_exception(exc) @@ -365,15 +356,6 @@ async def _sentry_wrapped_send( ) except Exception as exc: - suppress_chained_exceptions = ( - sentry_sdk.get_client() - .options.get("_experiments", {}) - .get("suppress_asgi_chained_exceptions", True) - ) - if suppress_chained_exceptions: - self._capture_request_exception(exc) - raise exc from None - exc_info = sys.exc_info() with capture_internal_exceptions(): self._capture_request_exception(exc) diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 4fc600d36f..c7e25a6a76 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -173,8 +173,8 @@ async def _task_with_sentry_span_creation() -> "Any": with span_ctx if span_ctx else nullcontext(): try: result = await coro - except StopAsyncIteration as e: - raise e from None + except StopAsyncIteration: + raise except Exception: reraise(*_capture_exception()) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index df287e6436..f3b1fd0898 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -188,10 +188,6 @@ async def sentry_wrapped_callback( if current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - integration = client.get_integration(DjangoIntegration) if not integration or not integration.middleware_spans: return await callback(request, *args, **kwargs) diff --git a/sentry_sdk/integrations/django/views.py b/sentry_sdk/integrations/django/views.py index b24e4df45b..eacd3b74fe 100644 --- a/sentry_sdk/integrations/django/views.py +++ b/sentry_sdk/integrations/django/views.py @@ -99,12 +99,6 @@ def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "A if current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - # set the active thread id to the handler thread for sync views - # this isn't necessary for async views since that runs on main - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - integration = client.get_integration(DjangoIntegration) if not integration or not integration.middleware_spans: return callback(request, *args, **kwargs) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 0b048e11ee..14ee66eecf 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -181,10 +181,6 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": elif current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - return old_call(*args, **kwargs) _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] diff --git a/sentry_sdk/integrations/opentelemetry/__init__.py b/sentry_sdk/integrations/opentelemetry/__init__.py deleted file mode 100644 index d76b8058ee..0000000000 --- a/sentry_sdk/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor - -__all__ = [ - "SentryPropagator", - "SentrySpanProcessor", -] diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py deleted file mode 100644 index d6733036ea..0000000000 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ /dev/null @@ -1,9 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable - -try: - from opentelemetry.context import create_key -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -SENTRY_TRACE_KEY = create_key("sentry-trace") -SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py deleted file mode 100644 index 472e8181f9..0000000000 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -IMPORTANT: The contents of this file are part of a proof of concept and as such -are experimental and not suitable for production use. They may be changed or -removed at any time without prior notice. -""" - -import warnings -from typing import TYPE_CHECKING - -from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import logger - -if TYPE_CHECKING: - from typing import Any, Dict, Optional - -try: - from opentelemetry import trace - from opentelemetry.propagate import set_global_textmap - from opentelemetry.sdk.trace import TracerProvider -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -try: - from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found] - DjangoInstrumentor, - ) -except ImportError: - DjangoInstrumentor = None - - -CONFIGURABLE_INSTRUMENTATIONS = { - DjangoInstrumentor: {"is_sql_commentor_enabled": True}, -} - - -class OpenTelemetryIntegration(Integration): - identifier = "opentelemetry" - - @staticmethod - def setup_once() -> None: - pass - - def setup_once_with_options( - self, options: "Optional[Dict[str, Any]]" = None - ) -> None: - if has_span_streaming_enabled(options): - logger.warning( - "[OTel] OpenTelemetryIntegration is not compatible with span streaming " - "(trace_lifecycle='stream') and will be disabled." - ) - return - - warnings.warn( - "OpenTelemetryIntegration is deprecated. " - "Please use OTLPIntegration instead: " - "https://docs.sentry.io/platforms/python/integrations/otlp/", - DeprecationWarning, - stacklevel=2, - ) - - _setup_sentry_tracing() - # _setup_instrumentors() - - logger.debug("[OTel] Finished setting up OpenTelemetry integration") - - -def _setup_sentry_tracing() -> None: - provider = TracerProvider() - provider.add_span_processor(SentrySpanProcessor()) - trace.set_tracer_provider(provider) - set_global_textmap(SentryPropagator()) - - -def _setup_instrumentors() -> None: - for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items(): - if instrumentor is None: - continue - instrumentor().instrument(**kwargs) diff --git a/sentry_sdk/integrations/opentelemetry/propagator.py b/sentry_sdk/integrations/opentelemetry/propagator.py deleted file mode 100644 index 5b5f13861d..0000000000 --- a/sentry_sdk/integrations/opentelemetry/propagator.py +++ /dev/null @@ -1,128 +0,0 @@ -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, -) -from sentry_sdk.tracing import ( - BAGGAGE_HEADER_NAME, - SENTRY_TRACE_HEADER_NAME, -) -from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data - -try: - from opentelemetry import trace - from opentelemetry.context import ( - Context, - get_current, - set_value, - ) - from opentelemetry.propagators.textmap import ( - CarrierT, - Getter, - Setter, - TextMapPropagator, - default_getter, - default_setter, - ) - from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Set - - -class SentryPropagator(TextMapPropagator): - """ - Propagates tracing headers for Sentry's tracing system in a way OTel understands. - """ - - def extract( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - getter: "Getter[CarrierT]" = default_getter, - ) -> "Context": - if context is None: - context = get_current() - - sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) - if not sentry_trace: - return context - - sentrytrace = extract_sentrytrace_data(sentry_trace[0]) - if not sentrytrace: - return context - - context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) - - trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] - - span_context = SpanContext( - trace_id=int(trace_id, 16), # type: ignore - span_id=int(span_id, 16), # type: ignore - # we simulate a sampled trace on the otel side and leave the sampling to sentry - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - - baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) - - if baggage_header: - baggage = Baggage.from_incoming_header(baggage_header[0]) - else: - # 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 frozen and won't be populated as head SDK. - baggage = Baggage(sentry_items={}) - - baggage.freeze() - context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) - - span = NonRecordingSpan(span_context) - modified_context = trace.set_span_in_context(span, context) - return modified_context - - def inject( - self, - carrier: "CarrierT", - context: "Optional[Context]" = None, - setter: "Setter[CarrierT]" = default_setter, - ) -> None: - if context is None: - context = get_current() - - current_span = trace.get_current_span(context) - current_span_context = current_span.get_span_context() - - if not current_span_context.is_valid: - return - - span_id = trace.format_span_id(current_span_context.span_id) - - span_map = SentrySpanProcessor.otel_span_map - sentry_span = span_map.get(span_id, None) - if not sentry_span: - return - - setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent()) - - if sentry_span.containing_transaction: - baggage = sentry_span.containing_transaction.get_baggage() - if baggage: - baggage_data = baggage.serialize() - if baggage_data: - setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) - - @property - def fields(self) -> "Set[str]": - return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py deleted file mode 100644 index 1efd2ac455..0000000000 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from time import time -from typing import TYPE_CHECKING, cast - -from urllib3.util import parse_url as urlparse - -from sentry_sdk import get_client, start_transaction -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.tracing import Span as SentrySpan -from sentry_sdk.tracing import Transaction - -try: - from opentelemetry.context import get_value - from opentelemetry.sdk.trace import ReadableSpan as OTelSpan - from opentelemetry.sdk.trace import SpanProcessor - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import ( - SpanKind, - format_span_id, - format_trace_id, - get_current_span, - ) - from opentelemetry.trace.span import ( - INVALID_SPAN_ID, - INVALID_TRACE_ID, - ) -except ImportError: - raise DidNotEnable("opentelemetry not installed") - -if TYPE_CHECKING: - from typing import Any, Optional, Union - - from opentelemetry import context as context_api - from opentelemetry.trace import SpanContext - - from sentry_sdk._types import Event, Hint - -OPEN_TELEMETRY_CONTEXT = "otel" -SPAN_MAX_TIME_OPEN_MINUTES = 10 -SPAN_ORIGIN = "auto.otel" - - -def link_trace_context_to_error_event( - event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" -) -> "Event": - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return event - - if hasattr(event, "type") and event["type"] == "transaction": - return event - - otel_span = get_current_span() - if not otel_span: - return event - - ctx = otel_span.get_span_context() - - if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID: - return event - - sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None) - if not sentry_span: - return event - - contexts = event.setdefault("contexts", {}) - contexts.setdefault("trace", {}).update(sentry_span.get_trace_context()) - - return event - - -class SentrySpanProcessor(SpanProcessor): - """ - Converts OTel spans into Sentry spans so they can be sent to the Sentry backend. - """ - - # The mapping from otel span ids to sentry spans - otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {} - - # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES - open_spans: "dict[int, set[str]]" = {} - - initialized: "bool" = False - - def __new__(cls) -> "SentrySpanProcessor": - if not hasattr(cls, "instance"): - cls.instance = super().__new__(cls) - - # "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute). - return cls.instance # type: ignore[misc] - - def __init__(self) -> None: - if self.initialized: - return - - @add_global_event_processor - def global_event_processor(event: "Event", hint: "Hint") -> "Event": - return link_trace_context_to_error_event(event, self.otel_span_map) - - self.initialized = True - - def _prune_old_spans(self: "SentrySpanProcessor") -> None: - """ - Prune spans that have been open for too long. - """ - current_time_minutes = int(time() / 60) - for span_start_minutes in list( - self.open_spans.keys() - ): # making a list because we change the dict - # prune empty open spans buckets - if self.open_spans[span_start_minutes] == set(): - self.open_spans.pop(span_start_minutes) - - # prune old buckets - elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES: - for span_id in self.open_spans.pop(span_start_minutes): - self.otel_span_map.pop(span_id, None) - - def on_start( - self, - otel_span: "OTelSpan", - parent_context: "Optional[context_api.Context]" = None, - ) -> None: - client = get_client() - - if not client.parsed_dsn: - return - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - if self._is_sentry_span(otel_span): - return - - trace_data = self._get_trace_data( - span_context, otel_span.parent, parent_context - ) - - parent_span_id = trace_data["parent_span_id"] - sentry_parent_span = ( - self.otel_span_map.get(parent_span_id) if parent_span_id else None - ) - - start_timestamp = None - if otel_span.start_time is not None: - start_timestamp = datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span = None - if sentry_parent_span: - sentry_span = sentry_parent_span.start_child( - span_id=trace_data["span_id"], - name=otel_span.name, - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - else: - sentry_span = start_transaction( - name=otel_span.name, - span_id=trace_data["span_id"], - parent_span_id=parent_span_id, - trace_id=trace_data["trace_id"], - baggage=trace_data["baggage"], - start_timestamp=start_timestamp, - instrumenter=INSTRUMENTER.OTEL, - origin=SPAN_ORIGIN, - ) - - self.otel_span_map[trace_data["span_id"]] = sentry_span - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).add( - trace_data["span_id"] - ) - - self._prune_old_spans() - - def on_end(self, otel_span: "OTelSpan") -> None: - client = get_client() - - if client.options["instrumenter"] != INSTRUMENTER.OTEL: - return - - span_context = otel_span.get_span_context() - if span_context is None or not span_context.is_valid: - return - - span_id = format_span_id(span_context.span_id) - sentry_span = self.otel_span_map.pop(span_id, None) - if not sentry_span: - return - - sentry_span.op = otel_span.name - - self._update_span_with_otel_status(sentry_span, otel_span) - - if isinstance(sentry_span, Transaction): - sentry_span.name = otel_span.name - sentry_span.set_context( - OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span) - ) - self._update_transaction_with_otel_data(sentry_span, otel_span) - - else: - self._update_span_with_otel_data(sentry_span, otel_span) - - end_timestamp = None - if otel_span.end_time is not None: - end_timestamp = datetime.fromtimestamp( - otel_span.end_time / 1e9, timezone.utc - ) # OTel spans have nanosecond precision - - sentry_span.finish(end_timestamp=end_timestamp) - - if otel_span.start_time is not None: - span_start_in_minutes = int( - otel_span.start_time / 1e9 / 60 - ) # OTel spans have nanosecond precision - self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id) - - self._prune_old_spans() - - def _is_sentry_span(self, otel_span: "OTelSpan") -> bool: - """ - Break infinite loop: - HTTP requests to Sentry are caught by OTel and send again to Sentry. - """ - otel_span_url = None - if otel_span.attributes is not None: - otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL) - otel_span_url = cast("Optional[str]", otel_span_url) - - parsed_dsn = get_client().parsed_dsn - dsn_url = parsed_dsn.netloc if parsed_dsn else None - - if otel_span_url and dsn_url and dsn_url in otel_span_url: - return True - - return False - - def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]": - """ - Returns the OTel context for Sentry. - See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context - """ - ctx = {} - - if otel_span.attributes: - ctx["attributes"] = dict(otel_span.attributes) - - if otel_span.resource.attributes: - ctx["resource"] = dict(otel_span.resource.attributes) - - return ctx - - def _get_trace_data( - self, - span_context: "SpanContext", - parent_span_context: "Optional[SpanContext]", - parent_context: "Optional[context_api.Context]", - ) -> "dict[str, Any]": - """ - Extracts tracing information from one OTel span's context and its parent OTel context. - """ - trace_data: "dict[str, Any]" = {} - - span_id = format_span_id(span_context.span_id) - trace_data["span_id"] = span_id - - trace_id = format_trace_id(span_context.trace_id) - trace_data["trace_id"] = trace_id - - parent_span_id = ( - format_span_id(parent_span_context.span_id) if parent_span_context else None - ) - trace_data["parent_span_id"] = parent_span_id - - sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context) - sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data) - trace_data["parent_sampled"] = ( - sentry_trace_data["parent_sampled"] if sentry_trace_data else None - ) - - baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context) - trace_data["baggage"] = baggage - - return trace_data - - def _update_span_with_otel_status( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Set the Sentry span status from the OTel span - """ - if otel_span.status.is_unset: - return - - if otel_span.status.is_ok: - sentry_span.set_status(SPANSTATUS.OK) - return - - sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR) - - def _update_span_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - """ - Convert OTel span data and update the Sentry span with it. - This should eventually happen on the server when ingesting the spans. - """ - sentry_span.set_data("otel.kind", otel_span.kind) - - op = otel_span.name - description = otel_span.name - - if otel_span.attributes is not None: - for key, val in otel_span.attributes.items(): - sentry_span.set_data(key, val) - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - http_method = cast("Optional[str]", http_method) - - db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM) - - if http_method: - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - description = http_method - - peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None) - if peer_name: - description += " {}".format(peer_name) - - target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None) - if target: - description += " {}".format(target) - - if not peer_name and not target: - url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None) - url = cast("Optional[str]", url) - if url: - parsed_url = urlparse(url) - url = "{}://{}{}".format( - parsed_url.scheme, parsed_url.netloc, parsed_url.path - ) - description += " {}".format(url) - - status_code = otel_span.attributes.get( - SpanAttributes.HTTP_STATUS_CODE, None - ) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - elif db_query: - op = "db" - statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None) - statement = cast("Optional[str]", statement) - if statement: - description = statement - - sentry_span.op = op - sentry_span.description = description - - def _update_transaction_with_otel_data( - self, sentry_span: "SentrySpan", otel_span: "OTelSpan" - ) -> None: - if otel_span.attributes is None: - return - - http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD) - - if http_method: - status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) - status_code = cast("Optional[int]", status_code) - if status_code: - sentry_span.set_http_status(status_code) - - op = "http" - - if otel_span.kind == SpanKind.SERVER: - op += ".server" - elif otel_span.kind == SpanKind.CLIENT: - op += ".client" - - sentry_span.op = op diff --git a/sentry_sdk/integrations/otlp.py b/sentry_sdk/integrations/otlp.py index b91f2cfd21..5b26752700 100644 --- a/sentry_sdk/integrations/otlp.py +++ b/sentry_sdk/integrations/otlp.py @@ -6,7 +6,7 @@ BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, ) -from sentry_sdk.tracing_utils import Baggage +from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data from sentry_sdk.utils import ( Dsn, capture_internal_exceptions, @@ -15,16 +15,22 @@ ) try: + from opentelemetry import trace from opentelemetry.context import ( Context, + create_key, get_current, get_value, + set_value, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.textmap import ( CarrierT, + Getter, Setter, + TextMapPropagator, + default_getter, default_setter, ) from opentelemetry.sdk.trace import Span, TracerProvider @@ -32,23 +38,27 @@ from opentelemetry.trace import ( INVALID_SPAN_ID, INVALID_TRACE_ID, + NonRecordingSpan, SpanContext, + TraceFlags, format_span_id, format_trace_id, get_current_span, get_tracer_provider, set_tracer_provider, ) - - from sentry_sdk.integrations.opentelemetry.consts import SENTRY_BAGGAGE_KEY - from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator except ImportError: raise DidNotEnable("opentelemetry-distro[otlp] is not installed") + from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple + from typing import Any, Dict, Optional, Set, Tuple + + +SENTRY_BAGGAGE_KEY = create_key("sentry-baggage") +SENTRY_TRACE_KEY = create_key("sentry-trace") def otel_propagation_context() -> "Optional[Tuple[str, str]]": @@ -121,11 +131,8 @@ def _sentry_patched_record_exception( _sentry_patched_exception = True -class SentryOTLPPropagator(SentryPropagator): +class SentryOTLPPropagator(TextMapPropagator): """ - We need to override the inject of the older propagator since that - is SpanProcessor based. - !!! Note regarding baggage: We cannot meaningfully populate a new baggage as a head SDK when we are using OTLP since we don't have any sort of transaction semantic to @@ -134,6 +141,52 @@ class SentryOTLPPropagator(SentryPropagator): For incoming baggage, we just pass it on as is so that case is correctly handled. """ + def extract( + self, + carrier: "CarrierT", + context: "Optional[Context]" = None, + getter: "Getter[CarrierT]" = default_getter, + ) -> "Context": + if context is None: + context = get_current() + + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + if not sentry_trace: + return context + + sentrytrace = extract_sentrytrace_data(sentry_trace[0]) + if not sentrytrace: + return context + + context = set_value(SENTRY_TRACE_KEY, sentrytrace, context) + + trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"] + + span_context = SpanContext( + trace_id=int(trace_id, 16), # type: ignore + span_id=int(span_id, 16), # type: ignore + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags=TraceFlags(TraceFlags.SAMPLED), + is_remote=True, + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + if baggage_header: + baggage = Baggage.from_incoming_header(baggage_header[0]) + else: + # 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 frozen and won't be populated as head SDK. + baggage = Baggage(sentry_items={}) + + baggage.freeze() + context = set_value(SENTRY_BAGGAGE_KEY, baggage, context) + + span = NonRecordingSpan(span_context) + modified_context = trace.set_span_in_context(span, context) + return modified_context + def inject( self, carrier: "CarrierT", @@ -162,6 +215,10 @@ def inject( if baggage_data: setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data) + @property + def fields(self) -> "Set[str]": + return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME} + def _to_traceparent(span_context: "SpanContext") -> str: """ diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 8c281f5874..c3238abe55 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -133,10 +133,6 @@ def _sentry_func(*args: "Any", **kwargs: "Any") -> "Any": if current_scope.transaction is not None: current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - return old_func(*args, **kwargs) return old_decorator(_sentry_func) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 2d839d1c61..9f1ece76c8 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -241,7 +241,7 @@ async def _context_exit( response_status = None if response is None else response.status # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception - # happens while trying to end the transaction, we still attempt to exit the hub. + # happens while trying to end the transaction, we still attempt to exit the scope. with capture_internal_exceptions(): span = request.ctx._sentry_root_span if isinstance(span, StreamedSpan): diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 21cdfeecc5..ef01e48346 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -622,8 +622,6 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": current_scope.transaction.update_active_thread() sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() request = args[0] diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index f3ba046332..606caa852e 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -11,7 +11,6 @@ from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - logger, reraise, ) @@ -27,23 +26,9 @@ class ThreadingIntegration(Integration): identifier = "threading" - def __init__( - self, propagate_hub: "Optional[bool]" = None, propagate_scope: bool = True - ) -> None: - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - + def __init__(self, propagate_scope: bool = True) -> None: self.propagate_scope = propagate_scope - if propagate_hub is not None: - self.propagate_scope = propagate_hub - @staticmethod def setup_once() -> None: old_start = Thread.start diff --git a/sentry_sdk/profiler/__init__.py b/sentry_sdk/profiler/__init__.py index d562405295..99d8cd2d61 100644 --- a/sentry_sdk/profiler/__init__.py +++ b/sentry_sdk/profiler/__init__.py @@ -4,17 +4,6 @@ stop_profile_session, stop_profiler, ) -from sentry_sdk.profiler.transaction_profiler import ( - MAX_PROFILE_DURATION_NS, - PROFILE_MINIMUM_SAMPLES, - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - has_profiling_enabled, - setup_profiler, - teardown_profiler, -) from sentry_sdk.profiler.utils import ( DEFAULT_SAMPLING_FREQUENCY, MAX_STACK_DEPTH, @@ -29,17 +18,6 @@ "start_profiler", "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` "stop_profiler", - # DEPRECATED: The following was re-exported for backwards compatibility. It - # will be removed from sentry_sdk.profiler in a future release. - "MAX_PROFILE_DURATION_NS", - "PROFILE_MINIMUM_SAMPLES", - "Profile", - "Scheduler", - "ThreadScheduler", - "GeventScheduler", - "has_profiling_enabled", - "setup_profiler", - "teardown_profiler", "DEFAULT_SAMPLING_FREQUENCY", "MAX_STACK_DEPTH", "get_frame_name", diff --git a/sentry_sdk/profiler/transaction_profiler.py b/sentry_sdk/profiler/transaction_profiler.py deleted file mode 100644 index fe65774c0b..0000000000 --- a/sentry_sdk/profiler/transaction_profiler.py +++ /dev/null @@ -1,802 +0,0 @@ -""" -This file is originally based on code from https://github.com/nylas/nylas-perftools, -which is published under the following license: - -The MIT License (MIT) - -Copyright (c) 2014 Nylas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -import atexit -import os -import platform -import random -import sys -import threading -import time -import uuid -import warnings -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING - -import sentry_sdk -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.utils import ( - DEFAULT_SAMPLING_FREQUENCY, - extract_stack, -) -from sentry_sdk.utils import ( - capture_internal_exception, - capture_internal_exceptions, - get_current_thread_meta, - is_gevent, - is_valid_sample_rate, - logger, - nanosecond_time, - set_in_app_in_frames, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Deque, Dict, List, Optional, Set, Type - - from typing_extensions import TypedDict - - from sentry_sdk._types import Event, ProfilerMode, SamplingContext - from sentry_sdk.profiler.utils import ( - ExtractedSample, - FrameId, - ProcessedFrame, - ProcessedStack, - ProcessedThreadMetadata, - StackId, - ThreadId, - ) - - ProcessedSample = TypedDict( - "ProcessedSample", - { - "elapsed_since_start_ns": str, - "thread_id": ThreadId, - "stack_id": int, - }, - ) - - ProcessedProfile = TypedDict( - "ProcessedProfile", - { - "frames": List[ProcessedFrame], - "stacks": List[ProcessedStack], - "samples": List[ProcessedSample], - "thread_metadata": Dict[ThreadId, ProcessedThreadMetadata], - }, - ) - - -try: - from gevent.monkey import get_original - from gevent.threadpool import ThreadPool as _ThreadPool - - ThreadPool: "Optional[Type[_ThreadPool]]" = _ThreadPool - thread_sleep = get_original("time", "sleep") -except ImportError: - thread_sleep = time.sleep - - ThreadPool = None - - -_scheduler: "Optional[Scheduler]" = None - - -# The minimum number of unique samples that must exist in a profile to be -# considered valid. -PROFILE_MINIMUM_SAMPLES = 2 - - -def has_profiling_enabled(options: "Dict[str, Any]") -> bool: - profiles_sampler = options["profiles_sampler"] - if profiles_sampler is not None: - return True - - profiles_sample_rate = options["profiles_sample_rate"] - if profiles_sample_rate is not None and profiles_sample_rate > 0: - return True - - profiles_sample_rate = options["_experiments"].get("profiles_sample_rate") - if profiles_sample_rate is not None: - logger.warning( - "_experiments['profiles_sample_rate'] is deprecated. " - "Please use the non-experimental profiles_sample_rate option " - "directly." - ) - if profiles_sample_rate > 0: - return True - - return False - - -def setup_profiler(options: "Dict[str, Any]") -> bool: - global _scheduler - - if _scheduler is not None: - logger.debug("[Profiling] Profiler is already setup") - return False - - frequency = DEFAULT_SAMPLING_FREQUENCY - - if is_gevent(): - # If gevent has patched the threading modules then we cannot rely on - # them to spawn a native thread for sampling. - # Instead we default to the GeventScheduler which is capable of - # spawning native threads within gevent. - default_profiler_mode = GeventScheduler.mode - else: - default_profiler_mode = ThreadScheduler.mode - - if options.get("profiler_mode") is not None: - profiler_mode = options["profiler_mode"] - else: - profiler_mode = options.get("_experiments", {}).get("profiler_mode") - if profiler_mode is not None: - logger.warning( - "_experiments['profiler_mode'] is deprecated. Please use the " - "non-experimental profiler_mode option directly." - ) - profiler_mode = profiler_mode or default_profiler_mode - - if ( - profiler_mode == ThreadScheduler.mode - # for legacy reasons, we'll keep supporting sleep mode for this scheduler - or profiler_mode == "sleep" - ): - _scheduler = ThreadScheduler(frequency=frequency) - elif profiler_mode == GeventScheduler.mode: - _scheduler = GeventScheduler(frequency=frequency) - else: - raise ValueError("Unknown profiler mode: {}".format(profiler_mode)) - - logger.debug( - "[Profiling] Setting up profiler in {mode} mode".format(mode=_scheduler.mode) - ) - _scheduler.setup() - - atexit.register(teardown_profiler) - - return True - - -def teardown_profiler() -> None: - global _scheduler - - if _scheduler is not None: - _scheduler.teardown() - - _scheduler = None - - -MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds - - -class Profile: - def __init__( - self, - sampled: "Optional[bool]", - start_ns: int, - hub: "Optional[sentry_sdk.Hub]" = None, - scheduler: "Optional[Scheduler]" = None, - ) -> None: - self.scheduler = _scheduler if scheduler is None else scheduler - - self.event_id: str = uuid.uuid4().hex - - self.sampled: "Optional[bool]" = sampled - - # Various framework integrations are capable of overwriting the active thread id. - # If it is set to `None` at the end of the profile, we fall back to the default. - self._default_active_thread_id: int = get_current_thread_meta()[0] or 0 - self.active_thread_id: "Optional[int]" = None - - try: - self.start_ns: int = start_ns - except AttributeError: - self.start_ns = 0 - - self.stop_ns: int = 0 - self.active: bool = False - - self.indexed_frames: "Dict[FrameId, int]" = {} - self.indexed_stacks: "Dict[StackId, int]" = {} - self.frames: "List[ProcessedFrame]" = [] - self.stacks: "List[ProcessedStack]" = [] - self.samples: "List[ProcessedSample]" = [] - - self.unique_samples = 0 - - # Backwards compatibility with the old hub property - self._hub: "Optional[sentry_sdk.Hub]" = None - if hub is not None: - self._hub = hub - warnings.warn( - "The `hub` parameter is deprecated. Please do not use it.", - DeprecationWarning, - stacklevel=2, - ) - - def update_active_thread_id(self) -> None: - self.active_thread_id = get_current_thread_meta()[0] - logger.debug( - "[Profiling] updating active thread id to {tid}".format( - tid=self.active_thread_id - ) - ) - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the profile's sampling decision according to the following - precedence rules: - - 1. If the transaction to be profiled is not sampled, that decision - will be used, regardless of anything else. - - 2. Use `profiles_sample_rate` to decide. - """ - - # The corresponding transaction was not sampled, - # so don't generate a profile for it. - if not self.sampled: - logger.debug( - "[Profiling] Discarding profile because transaction is discarded." - ) - self.sampled = False - return - - # The profiler hasn't been properly initialized. - if self.scheduler is None: - logger.debug( - "[Profiling] Discarding profile because profiler was not started." - ) - self.sampled = False - return - - client = sentry_sdk.get_client() - if not client.is_active(): - self.sampled = False - return - - options = client.options - - if callable(options.get("profiles_sampler")): - sample_rate = options["profiles_sampler"](sampling_context) - elif options["profiles_sample_rate"] is not None: - sample_rate = options["profiles_sample_rate"] - else: - sample_rate = options["_experiments"].get("profiles_sample_rate") - - # The profiles_sample_rate option was not set, so profiling - # was never enabled. - if sample_rate is None: - logger.debug( - "[Profiling] Discarding profile because profiling was not enabled." - ) - self.sampled = False - return - - if not is_valid_sample_rate(sample_rate, source="Profiling"): - logger.warning( - "[Profiling] Discarding profile because of invalid sample rate." - ) - self.sampled = False - return - - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < float(sample_rate) - - if self.sampled: - logger.debug("[Profiling] Initializing profile") - else: - logger.debug( - "[Profiling] Discarding profile because it's not included in the random sample (sample rate = {sample_rate})".format( - sample_rate=float(sample_rate) - ) - ) - - def start(self) -> None: - if not self.sampled or self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Starting profile") - self.active = True - if not self.start_ns: - self.start_ns = nanosecond_time() - self.scheduler.start_profiling(self) - - def stop(self) -> None: - if not self.sampled or not self.active: - return - - assert self.scheduler, "No scheduler specified" - logger.debug("[Profiling] Stopping profile") - self.active = False - self.stop_ns = nanosecond_time() - - def __enter__(self) -> "Profile": - scope = sentry_sdk.get_isolation_scope() - old_profile = scope.profile - scope.profile = self - - self._context_manager_state = (scope, old_profile) - - self.start() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - with capture_internal_exceptions(): - self.stop() - - scope, old_profile = self._context_manager_state - del self._context_manager_state - - scope.profile = old_profile - - def write(self, ts: int, sample: "ExtractedSample") -> None: - if not self.active: - return - - if ts < self.start_ns: - return - - offset = ts - self.start_ns - if offset > MAX_PROFILE_DURATION_NS: - self.stop() - return - - self.unique_samples += 1 - - elapsed_since_start_ns = str(offset) - - for tid, (stack_id, frame_ids, frames) in sample: - try: - # Check if the stack is indexed first, this lets us skip - # indexing frames if it's not necessary - if stack_id not in self.indexed_stacks: - for i, frame_id in enumerate(frame_ids): - if frame_id not in self.indexed_frames: - self.indexed_frames[frame_id] = len(self.indexed_frames) - self.frames.append(frames[i]) - - self.indexed_stacks[stack_id] = len(self.indexed_stacks) - self.stacks.append( - [self.indexed_frames[frame_id] for frame_id in frame_ids] - ) - - self.samples.append( - { - "elapsed_since_start_ns": elapsed_since_start_ns, - "thread_id": tid, - "stack_id": self.indexed_stacks[stack_id], - } - ) - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - - def process(self) -> "ProcessedProfile": - # This collects the thread metadata at the end of a profile. Doing it - # this way means that any threads that terminate before the profile ends - # will not have any metadata associated with it. - thread_metadata: "Dict[str, ProcessedThreadMetadata]" = { - str(thread.ident): { - "name": str(thread.name), - } - for thread in threading.enumerate() - } - - return { - "frames": self.frames, - "stacks": self.stacks, - "samples": self.samples, - "thread_metadata": thread_metadata, - } - - def to_json( - self, event_opt: "Event", options: "Dict[str, Any]" - ) -> "Dict[str, Any]": - profile = self.process() - - set_in_app_in_frames( - profile["frames"], - options["in_app_exclude"], - options["in_app_include"], - options["project_root"], - ) - - return { - "environment": event_opt.get("environment"), - "event_id": self.event_id, - "platform": "python", - "profile": profile, - "release": event_opt.get("release", ""), - "timestamp": event_opt["start_timestamp"], - "version": "1", - "device": { - "architecture": platform.machine(), - }, - "os": { - "name": platform.system(), - "version": platform.release(), - }, - "runtime": { - "name": platform.python_implementation(), - "version": platform.python_version(), - }, - "transactions": [ - { - "id": event_opt["event_id"], - "name": event_opt["transaction"], - # we start the transaction before the profile and this is - # the transaction start time relative to the profile, so we - # hardcode it to 0 until we can start the profile before - "relative_start_ns": "0", - # use the duration of the profile instead of the transaction - # because we end the transaction after the profile - "relative_end_ns": str(self.stop_ns - self.start_ns), - "trace_id": event_opt["contexts"]["trace"]["trace_id"], - "active_thread_id": str( - self._default_active_thread_id - if self.active_thread_id is None - else self.active_thread_id - ), - } - ], - } - - def valid(self) -> bool: - client = sentry_sdk.get_client() - if not client.is_active(): - return False - - if not has_profiling_enabled(client.options): - return False - - if self.sampled is None or not self.sampled: - if client.transport: - client.transport.record_lost_event( - "sample_rate", data_category="profile" - ) - return False - - if self.unique_samples < PROFILE_MINIMUM_SAMPLES: - if client.transport: - client.transport.record_lost_event( - "insufficient_data", data_category="profile" - ) - logger.debug("[Profiling] Discarding profile because insufficient samples.") - return False - - return True - - @property - def hub(self) -> "Optional[sentry_sdk.Hub]": - warnings.warn( - "The `hub` attribute is deprecated. Please do not access it.", - DeprecationWarning, - stacklevel=2, - ) - return self._hub - - @hub.setter - def hub(self, value: "Optional[sentry_sdk.Hub]") -> None: - warnings.warn( - "The `hub` attribute is deprecated. Please do not set it.", - DeprecationWarning, - stacklevel=2, - ) - self._hub = value - - -class Scheduler(ABC): - mode: "ProfilerMode" = "unknown" - - def __init__(self, frequency: int) -> None: - self.interval = 1.0 / frequency - - self.sampler = self.make_sampler() - - # cap the number of new profiles at any time so it does not grow infinitely - self.new_profiles: "Deque[Profile]" = deque(maxlen=128) - self.active_profiles: "Set[Profile]" = set() - - def __enter__(self) -> "Scheduler": - self.setup() - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - self.teardown() - - @abstractmethod - def setup(self) -> None: - pass - - @abstractmethod - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - """ - Ensure the scheduler is running. By default, this method is a no-op. - The method should be overridden by any implementation for which it is - relevant. - """ - return None - - def start_profiling(self, profile: "Profile") -> None: - self.ensure_running() - self.new_profiles.append(profile) - - def make_sampler(self) -> "Callable[..., None]": - cwd = os.getcwd() - - cache = LRUCache(max_size=256) - - def _sample_stack(*args: "Any", **kwargs: "Any") -> None: - """ - Take a sample of the stack on all the threads in the process. - This should be called at a regular interval to collect samples. - """ - # no profiles taking place, so we can stop early - if not self.new_profiles and not self.active_profiles: - # make sure to clear the cache if we're not profiling so we dont - # keep a reference to the last stack of frames around - return - - # This is the number of profiles we want to pop off. - # It's possible another thread adds a new profile to - # the list and we spend longer than we want inside - # the loop below. - # - # Also make sure to set this value before extracting - # frames so we do not write to any new profiles that - # were started after this point. - new_profiles = len(self.new_profiles) - - now = nanosecond_time() - - try: - sample = [ - (str(tid), extract_stack(frame, cache, cwd)) - for tid, frame in sys._current_frames().items() - ] - except AttributeError: - # For some reason, the frame we get doesn't have certain attributes. - # When this happens, we abandon the current sample as it's bad. - capture_internal_exception(sys.exc_info()) - return - - # Move the new profiles into the active_profiles set. - # - # We cannot directly add the to active_profiles set - # in `start_profiling` because it is called from other - # threads which can cause a RuntimeError when it the - # set sizes changes during iteration without a lock. - # - # We also want to avoid using a lock here so threads - # that are starting profiles are not blocked until it - # can acquire the lock. - for _ in range(new_profiles): - self.active_profiles.add(self.new_profiles.popleft()) - - inactive_profiles = [] - - for profile in self.active_profiles: - if profile.active: - profile.write(now, sample) - else: - # If a profile is marked inactive, we buffer it - # to `inactive_profiles` so it can be removed. - # We cannot remove it here as it would result - # in a RuntimeError. - inactive_profiles.append(profile) - - for profile in inactive_profiles: - self.active_profiles.remove(profile) - - return _sample_stack - - -class ThreadScheduler(Scheduler): - """ - This scheduler is based on running a daemon thread that will call - the sampler at a regular interval. - """ - - mode: "ProfilerMode" = "thread" - name = "sentry.profiler.ThreadScheduler" - - def __init__(self, frequency: int) -> None: - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[threading.Thread]" = None - self.pid: "Optional[int]" = None - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - """ - Check that the profiler has an active thread to run in, and start one if - that's not the case. - - Note that this might fail (e.g. in Python 3.12 it's not possible to - spawn new threads at interpreter shutdown). In that case self.running - will be False after running this function. - """ - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - # make sure the thread is a daemon here otherwise this - # can keep the application running after other threads - # have exited - self.thread = threading.Thread(name=self.name, target=self.run, daemon=True) - try: - self.thread.start() - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() - - -class GeventScheduler(Scheduler): - """ - This scheduler is based on the thread scheduler but adapted to work with - gevent. When using gevent, it may monkey patch the threading modules - (`threading` and `_thread`). This results in the use of greenlets instead - of native threads. - - This is an issue because the sampler CANNOT run in a greenlet because - 1. Other greenlets doing sync work will prevent the sampler from running - 2. The greenlet runs in the same thread as other greenlets so when taking - a sample, other greenlets will have been evicted from the thread. This - results in a sample containing only the sampler's code. - """ - - mode: "ProfilerMode" = "gevent" - name = "sentry.profiler.GeventScheduler" - - def __init__(self, frequency: int) -> None: - if ThreadPool is None: - raise ValueError("Profiler mode: {} is not available".format(self.mode)) - - super().__init__(frequency=frequency) - - # used to signal to the thread that it should stop - self.running = False - self.thread: "Optional[_ThreadPool]" = None - self.pid: "Optional[int]" = None - - # This intentionally uses the gevent patched threading.Lock. - # The lock will be required when first trying to start profiles - # as we need to spawn the profiler thread from the greenlets. - self.lock = threading.Lock() - - def setup(self) -> None: - pass - - def teardown(self) -> None: - if self.running: - self.running = False - if self.thread is not None: - self.thread.join() - - def ensure_running(self) -> None: - pid = os.getpid() - - # is running on the right process - if self.running and self.pid == pid: - return - - with self.lock: - # another thread may have tried to acquire the lock - # at the same time so it may start another thread - # make sure to check again before proceeding - if self.running and self.pid == pid: - return - - self.pid = pid - self.running = True - - self.thread = ThreadPool(1) # type: ignore[misc] - try: - self.thread.spawn(self.run) - except RuntimeError: - # Unfortunately at this point the interpreter is in a state that no - # longer allows us to spawn a thread and we have to bail. - self.running = False - self.thread = None - return - - def run(self) -> None: - last = time.perf_counter() - - while self.running: - self.sampler() - - # some time may have elapsed since the last time - # we sampled, so we need to account for that and - # not sleep for too long - elapsed = time.perf_counter() - last - if elapsed < self.interval: - thread_sleep(self.interval - elapsed) - - # after sleeping, make sure to take the current - # timestamp so we can use it next iteration - last = time.perf_counter() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 09e8876d6d..d1be9f53db 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -17,7 +17,6 @@ from sentry_sdk.consts import ( DEFAULT_MAX_BREADCRUMBS, FALSE_VALUES, - INSTRUMENTER, SPANDATA, ) from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer @@ -26,7 +25,6 @@ try_autostart_continuous_profiler, try_profile_lifecycle_trace_start, ) -from sentry_sdk.profiler.transaction_profiler import Profile from sentry_sdk.session import Session from sentry_sdk.traces import ( _DEFAULT_PARENT_SPAN, @@ -139,7 +137,7 @@ class ScopeType(Enum): class _ScopeManager: - def __init__(self, hub: "Optional[Any]" = None) -> None: + def __init__(self) -> None: self._old_scopes: "List[Scope]" = [] def __enter__(self) -> "Scope": @@ -235,7 +233,6 @@ class Scope: "_session", "_attachments", "_force_auto_session_tracking", - "_profile", "_propagation_context", "client", "_type", @@ -302,8 +299,6 @@ def __copy__(self) -> "Scope": rv._force_auto_session_tracking = self._force_auto_session_tracking rv._attachments = self._attachments.copy() - rv._profile = self._profile - rv._last_event_id = self._last_event_id rv._flags = deepcopy(self._flags) @@ -758,8 +753,6 @@ def clear(self) -> None: self._session: "Optional[Session]" = None self._force_auto_session_tracking: "Optional[bool]" = None - self._profile: "Optional[Profile]" = None - self._propagation_context = None # self._last_event_id is only applicable to isolation scopes @@ -932,14 +925,6 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: span._attributes["sentry.segment.name.source"] ) - @property - def profile(self) -> "Optional[Profile]": - return self._profile - - @profile.setter - def profile(self, profile: "Optional[Profile]") -> None: - self._profile = profile - def set_tag(self, key: str, value: "Any") -> None: """ Sets a tag for a key to a specific value. @@ -1107,7 +1092,6 @@ def add_breadcrumb( def start_transaction( self, transaction: "Optional[Transaction]" = None, - instrumenter: str = INSTRUMENTER.SENTRY, custom_sampling_context: "Optional[SamplingContext]" = None, **kwargs: "Unpack[TransactionKwargs]", ) -> "Union[Transaction, NoOpSpan]": @@ -1135,8 +1119,6 @@ def start_transaction( :param transaction: The transaction to start. If omitted, we create and start a new transaction. - :param instrumenter: This parameter is meant for internal use only. It - will be removed in the next major version. :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 @@ -1153,11 +1135,6 @@ def start_transaction( kwargs.setdefault("scope", self) - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - try_autostart_continuous_profiler() custom_sampling_context = custom_sampling_context or {} @@ -1193,13 +1170,6 @@ def start_transaction( ) if transaction.sampled: - profile = Profile( - transaction.sampled, transaction._start_timestamp_monotonic_ns - ) - profile._set_initial_sampling_decision(sampling_context=sampling_context) - - transaction._profile = profile - transaction._continuous_profile = try_profile_lifecycle_trace_start() # Typically, the profiler is set when the transaction is created. But when @@ -1215,9 +1185,7 @@ def start_transaction( return transaction - def start_span( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": + def start_span(self, **kwargs: "Any") -> "Span": """ Start a span whose parent is the currently active span or transaction, if any. @@ -1232,10 +1200,6 @@ def start_span( one is not already in progress. For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. """ client = sentry_sdk.get_client() if has_span_streaming_enabled(client.options): @@ -1258,11 +1222,6 @@ def start_span( client = self.get_client() - configuration_instrumenter = client.options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - # get current span or transaction span = self.span or self.get_isolation_scope().span if isinstance(span, StreamedSpan): @@ -1964,8 +1923,6 @@ def update_from_scope(self, scope: "Scope") -> None: self._span = scope._span if scope._attachments: self._attachments.extend(scope._attachments) - if scope._profile: - self._profile = scope._profile if scope._propagation_context: self._propagation_context = scope._propagation_context if scope._session: diff --git a/sentry_sdk/sessions.py b/sentry_sdk/sessions.py index aabf874fcd..ea4a9a8757 100644 --- a/sentry_sdk/sessions.py +++ b/sentry_sdk/sessions.py @@ -1,5 +1,4 @@ import os -import warnings from contextlib import contextmanager from threading import Event, Lock, Thread from typing import TYPE_CHECKING @@ -10,76 +9,7 @@ from sentry_sdk.utils import format_timestamp if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, List, Optional, Union - - -def is_auto_session_tracking_enabled( - hub: "Optional[sentry_sdk.Hub]" = None, -) -> "Union[Any, bool, None]": - """DEPRECATED: Utility function to find out if session tracking is enabled.""" - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - - should_track = hub.scope._force_auto_session_tracking - - if should_track is None: - client_options = hub.client.options if hub.client else {} - should_track = client_options.get("auto_session_tracking", False) - - return should_track - - -@contextmanager -def auto_session_tracking( - hub: "Optional[sentry_sdk.Hub]" = None, session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: Use track_session instead - Starts and stops a session automatically around a block. - """ - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "Use track_session instead.", - DeprecationWarning, - stacklevel=2, - ) - - if hub is None: - hub = sentry_sdk.Hub.current - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - should_track = is_auto_session_tracking_enabled(hub) - if should_track: - hub.start_session(session_mode=session_mode) - try: - yield - finally: - if should_track: - hub.end_session() - - -def is_auto_session_tracking_enabled_scope(scope: "sentry_sdk.Scope") -> bool: - """ - DEPRECATED: Utility function to find out if session tracking is enabled. - """ - - warnings.warn( - "This function is deprecated and will be removed in the next major release. " - "There is no public API replacement.", - DeprecationWarning, - stacklevel=2, - ) - - # Internal callers should use private _is_auto_session_tracking_enabled, instead. - return _is_auto_session_tracking_enabled(scope) + from typing import Any, Callable, Dict, Generator, List, Optional def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: @@ -95,24 +25,6 @@ def _is_auto_session_tracking_enabled(scope: "sentry_sdk.Scope") -> bool: return should_track -@contextmanager -def auto_session_tracking_scope( - scope: "sentry_sdk.Scope", session_mode: str = "application" -) -> "Generator[None, None, None]": - """DEPRECATED: This function is a deprecated alias for track_session. - Starts and stops a session automatically around a block. - """ - - warnings.warn( - "This function is a deprecated alias for track_session and will be removed in the next major release.", - DeprecationWarning, - stacklevel=2, - ) - - with track_session(scope, session_mode=session_mode): - yield - - @contextmanager def track_session( scope: "sentry_sdk.Scope", session_mode: str = "application" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 5c088b936b..001d66f40e 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, cast import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.consts import SPANDATA, SPANSTATUS, SPANTEMPLATE from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( capture_internal_exceptions, @@ -43,7 +43,6 @@ SamplingContext, ) from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - from sentry_sdk.profiler.transaction_profiler import Profile class SpanKwargs(TypedDict, total=False): trace_id: str @@ -76,9 +75,6 @@ class SpanKwargs(TypedDict, total=False): description: str """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - hub: "Optional[sentry_sdk.Hub]" - """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" - status: str """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" @@ -240,7 +236,6 @@ class Span: .. deprecated:: 2.15.0 Please use the `name` parameter, instead. :param name: A string describing what operation is being performed within the span. - :param hub: The hub to use for this span. .. deprecated:: 2.0.0 Please use the `scope` parameter, instead. @@ -268,7 +263,6 @@ class Span: "_tags", "_data", "_span_recorder", - "hub", "_context_manager_state", "_containing_transaction", "scope", @@ -287,7 +281,6 @@ def __init__( sampled: "Optional[bool]" = None, op: "Optional[str]" = None, description: "Optional[str]" = None, - hub: "Optional[sentry_sdk.Hub]" = None, # deprecated status: "Optional[str]" = None, containing_transaction: "Optional[Transaction]" = None, start_timestamp: "Optional[Union[datetime, float]]" = None, @@ -303,7 +296,6 @@ def __init__( self.op = op self.description = name or description self.status = status - self.hub = hub # backwards compatibility self.scope = scope self.origin = origin self._measurements: "Dict[str, MeasurementValue]" = {} @@ -313,15 +305,6 @@ def __init__( self._flags: "Dict[str, bool]" = {} self._flags_capacity = 10 - if hub is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use `scope` instead.", - DeprecationWarning, - stacklevel=2, - ) - - self.scope = self.scope or hub.scope - if start_timestamp is None: start_timestamp = datetime.now(timezone.utc) elif isinstance(start_timestamp, float): @@ -429,19 +412,13 @@ def containing_transaction(self) -> "Optional[Transaction]": # referencing themselves) return self._containing_transaction - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "Span": + 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. - - The instrumenter parameter is deprecated for user code, and it will - be removed in the next major version. Going forward, it should only - be used by the SDK itself. """ if kwargs.get("description") is not None: warnings.warn( @@ -450,11 +427,6 @@ def start_child( stacklevel=2, ) - configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] - - if instrumenter != configuration_instrumenter: - return NoOpSpan() - kwargs.setdefault("sampled", self.sampled) child = Span( @@ -817,7 +789,6 @@ class Transaction(Span): "sample_rate", "_measurements", "_contexts", - "_profile", "_continuous_profile", "_baggage", "_sample_rand", @@ -839,7 +810,6 @@ def __init__( # type: ignore[misc] self.parent_sampled = parent_sampled self._measurements: "Dict[str, MeasurementValue]" = {} self._contexts: "Dict[str, Any]" = {} - self._profile: "Optional[Profile]" = None self._continuous_profile: "Optional[ContinuousProfile]" = None self._baggage = baggage @@ -888,17 +858,11 @@ def __enter__(self) -> "Transaction": super().__enter__() - if self._profile is not None: - self._profile.__enter__() - return self def __exit__( self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" ) -> None: - if self._profile is not None: - self._profile.__exit__(ty, value, tb) - if self._continuous_profile is not None: self._continuous_profile.stop() @@ -915,38 +879,6 @@ def containing_transaction(self) -> "Transaction": # reference. return self - def _get_scope_from_finish_args( - self, - scope_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - hub_arg: "Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]", - ) -> "Optional[sentry_sdk.Scope]": - """ - Logic to get the scope from the arguments passed to finish. This - function exists for backwards compatibility with the old finish. - - TODO: Remove this function in the next major version. - """ - scope_or_hub = scope_arg - if hub_arg is not None: - warnings.warn( - "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", - DeprecationWarning, - stacklevel=3, - ) - - scope_or_hub = hub_arg - - if isinstance(scope_or_hub, sentry_sdk.Hub): - warnings.warn( - "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", - DeprecationWarning, - stacklevel=3, - ) - - return scope_or_hub.scope - - return scope_or_hub - def _get_log_representation(self) -> str: return "{op}transaction <{name}>".format( op=("<" + self.op + "> " if self.op else ""), name=self.name @@ -956,8 +888,6 @@ def finish( self, scope: "Optional[sentry_sdk.Scope]" = None, end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, ) -> "Optional[str]": """Finishes the transaction and sends it to Sentry. All finished spans in the transaction will also be sent to Sentry. @@ -966,9 +896,6 @@ def finish( If not provided, the current Scope will be used. :param end_timestamp: Optional timestamp that should be used as timestamp instead of the current time. - :param hub: The hub to use for this transaction. - This argument is DEPRECATED. Please use the `scope` - parameter, instead. :return: The event ID if the transaction was sent to Sentry, otherwise None. @@ -977,10 +904,6 @@ def finish( # This transaction is already finished, ignore. return None - # For backwards compatibility, we must handle the case where `scope` - # or `hub` could both either be a `Scope` or a `Hub`. - scope = self._get_scope_from_finish_args(scope, hub) - scope = scope or self.scope or sentry_sdk.get_current_scope() client = sentry_sdk.get_client() @@ -1105,10 +1028,6 @@ def finish( if has_gen_ai_span: event["_has_gen_ai_span"] = True - if self._profile is not None and self._profile.valid(): - event["profile"] = self._profile - self._profile = None - event["measurements"] = self._measurements return scope.capture_event(event) @@ -1297,9 +1216,7 @@ def __repr__(self) -> str: def containing_transaction(self) -> "Optional[Transaction]": return None - def start_child( - self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "Any" - ) -> "NoOpSpan": + def start_child(self, **kwargs: "Any") -> "NoOpSpan": return NoOpSpan() def to_traceparent(self) -> str: @@ -1345,12 +1262,7 @@ def finish( self, scope: "Optional[sentry_sdk.Scope]" = None, end_timestamp: "Optional[Union[float, datetime]]" = None, - *, - hub: "Optional[sentry_sdk.Hub]" = None, ) -> "Optional[str]": - """ - The `hub` parameter is deprecated. Please use the `scope` parameter, instead. - """ pass def set_measurement( diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 2b71fee429..d98b8597fa 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -42,7 +42,6 @@ import certifi import urllib3 -import sentry_sdk from sentry_sdk.consts import EndpointType from sentry_sdk.envelope import Envelope, Item, PayloadRef from sentry_sdk.utils import ( @@ -234,9 +233,6 @@ def __init__(self: "Self", options: "Dict[str, Any]") -> None: self._pool = self._make_pool() - # Backwards compatibility for deprecated `self.hub_class` attribute - self._hub_cls = sentry_sdk.Hub - experiments = options.get("_experiments", {}) compression_level = experiments.get( "transport_compression_level", @@ -715,27 +711,6 @@ def flush( self._worker.submit(lambda: self._flush_client_reports(force=True)) self._worker.flush(timeout, callback) - @staticmethod - def _warn_hub_cls() -> None: - """Convenience method to warn users about the deprecation of the `hub_cls` attribute.""" - warnings.warn( - "The `hub_cls` attribute is deprecated and will be removed in a future release.", - DeprecationWarning, - stacklevel=3, - ) - - @property - def hub_cls(self: "Self") -> "type[sentry_sdk.Hub]": - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - return self._hub_cls - - @hub_cls.setter - def hub_cls(self: "Self", value: "type[sentry_sdk.Hub]") -> None: - """DEPRECATED: This attribute is deprecated and will be removed in a future release.""" - HttpTransport._warn_hub_cls() - self._hub_cls = value - class HttpTransport(BaseHttpTransport): if TYPE_CHECKING: diff --git a/setup.py b/setup.py index 35d95ff636..fa290a0962 100644 --- a/setup.py +++ b/setup.py @@ -73,8 +73,6 @@ def get_file_text(file_name): "mcp": ["mcp>=1.15.0"], "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], "openfeature": ["openfeature-sdk>=0.7.1"], - "opentelemetry": ["opentelemetry-distro>=0.35b0"], - "opentelemetry-experimental": ["opentelemetry-distro"], "opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"], "pure-eval": ["pure_eval", "executing", "asttokens"], "pydantic_ai": ["pydantic-ai>=1.0.0"], @@ -91,11 +89,6 @@ def get_file_text(file_name): "unleash": ["UnleashClient>=6.0.1"], "google-genai": ["google-genai>=1.29.0"], }, - entry_points={ - "opentelemetry_propagator": [ - "sentry=sentry_sdk.integrations.opentelemetry:SentryPropagator" - ] - }, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", diff --git a/tests/conftest.py b/tests/conftest.py index 6b406d6a06..04debe9b8c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,7 +61,6 @@ _installed_integrations, _processed_integrations, ) -from sentry_sdk.profiler import teardown_profiler from sentry_sdk.profiler.continuous_profiler import teardown_continuous_profiler from sentry_sdk.transport import Transport from sentry_sdk.utils import package_version, reraise @@ -272,12 +271,7 @@ def reset_integrations(): but this also means some other stuff will be monkeypatched twice. """ global _DEFAULT_INTEGRATIONS, _processed_integrations - try: - _DEFAULT_INTEGRATIONS.remove( - "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration" - ) - except ValueError: - pass + _processed_integrations.clear() _installed_integrations.clear() @@ -776,13 +770,11 @@ def __ne__(self, test_obj): @pytest.fixture def teardown_profiling(): # Make sure that a previous test didn't leave the profiler running - teardown_profiler() teardown_continuous_profiler() yield - # Make sure that to shut down the profiler after the test - teardown_profiler() + # Make sure to shut down the profiler after the test teardown_continuous_profiler() diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index 70a4b498d7..d97ba3265f 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -7,28 +7,16 @@ def capture_exceptions(monkeypatch): def inner(): errors = set() - old_capture_event_hub = sentry_sdk.Hub.capture_event - old_capture_event_scope = sentry_sdk.Scope.capture_event + old_capture_event = sentry_sdk.Scope.capture_event - def capture_event_hub(self, event, hint=None, scope=None): - """ - Can be removed when we remove push_scope and the Hub from the SDK. - """ + def capture_event(self, event, hint=None, scope=None): if hint: if "exc_info" in hint: error = hint["exc_info"][1] errors.add(error) - return old_capture_event_hub(self, event, hint=hint, scope=scope) + return old_capture_event(self, event, hint=hint, scope=scope) - def capture_event_scope(self, event, hint=None, scope=None): - if hint: - if "exc_info" in hint: - error = hint["exc_info"][1] - errors.add(error) - return old_capture_event_scope(self, event, hint=hint, scope=scope) - - monkeypatch.setattr(sentry_sdk.Hub, "capture_event", capture_event_hub) - monkeypatch.setattr(sentry_sdk.Scope, "capture_event", capture_event_scope) + monkeypatch.setattr(sentry_sdk.Scope, "capture_event", capture_event) return errors diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 8c58a5c6a6..8ad22a1057 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1,10 +1,8 @@ import asyncio import base64 import inspect -import json import os import sys -from unittest import mock import django import pytest @@ -197,85 +195,6 @@ async def test_async_views( } -@pytest.mark.parametrize("application", APPS) -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@pytest.mark.parametrize("middleware_spans", [False, True]) -@pytest.mark.asyncio -@pytest.mark.skipif( - django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" -) -@pytest.mark.parametrize("span_streaming", [True, False]) -async def test_active_thread_id( - sentry_init, - capture_envelopes, - capture_items, - teardown_profiling, - endpoint, - application, - middleware_spans, - span_streaming, -): - sentry_init( - integrations=[DjangoIntegration(middleware_spans=middleware_spans)], - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - trace_lifecycle="stream" if span_streaming else "static", - ) - - comm = HttpCommunicator(application, "GET", endpoint) - - if span_streaming: - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - items = capture_items("span") - response = await comm.get_response() - await comm.wait() - - assert response["status"] == 200, response["body"] - - data = json.loads(response["body"]) - - sentry_sdk.flush() - spans = [item.payload for item in items] - - for span in spans: - if span["is_segment"] is False: - continue - assert str(data["active"]) == span["attributes"]["thread.id"] - else: - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - envelopes = capture_envelopes() - response = await comm.get_response() - await comm.wait() - - assert response["status"] == 200, response["body"] - - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - data = json.loads(response["body"]) - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [ - item for item in envelopes[0].items if item.type == "transaction" - ] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - @pytest.mark.asyncio @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 41424bc27f..2e7383e758 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -4,7 +4,6 @@ import os import threading import warnings -from unittest import mock import fastapi import pytest @@ -464,44 +463,6 @@ def test_legacy_setup( assert event["transaction"] == "/message/{message_id}" -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) - app = fastapi_app_factory() - asgi_app = SentryAsgiMiddleware(app) - - envelopes = capture_envelopes() - - client = TestClient(asgi_app) - response = client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(response.content) - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [item for item in envelopes[0].items if item.type == "transaction"] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): sentry_init( diff --git a/tests/integrations/opentelemetry/__init__.py b/tests/integrations/opentelemetry/__init__.py deleted file mode 100644 index 75763c2fee..0000000000 --- a/tests/integrations/opentelemetry/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import pytest - -pytest.importorskip("opentelemetry") diff --git a/tests/integrations/opentelemetry/test_entry_points.py b/tests/integrations/opentelemetry/test_entry_points.py deleted file mode 100644 index 77d9898278..0000000000 --- a/tests/integrations/opentelemetry/test_entry_points.py +++ /dev/null @@ -1,26 +0,0 @@ -try: - from importlib.metadata import entry_points -except ImportError: - from importlib_metadata import entry_points - -from sentry_sdk.integrations.opentelemetry import SentryPropagator - - -def test_propagator_registered_as_entry_point(): - all_eps = entry_points() - - if isinstance(all_eps, dict): - # Python 3.7-3.8 stdlib returns a dict keyed by group - matches = [ - ep - for ep in all_eps.get("opentelemetry_propagator", []) - if ep.name == "sentry" - ] - else: - matches = list(all_eps.select(group="opentelemetry_propagator", name="sentry")) - - assert len(matches) >= 1 - assert matches[0].value == "sentry_sdk.integrations.opentelemetry:SentryPropagator" - - loaded = matches[0].load() - assert loaded is SentryPropagator diff --git a/tests/integrations/opentelemetry/test_experimental.py b/tests/integrations/opentelemetry/test_experimental.py deleted file mode 100644 index 905ab0d5b4..0000000000 --- a/tests/integrations/opentelemetry/test_experimental.py +++ /dev/null @@ -1,57 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest - -from sentry_sdk.integrations.opentelemetry.integration import OpenTelemetryIntegration - - -@pytest.mark.forked -def test_integration_enabled_if_option_is_on(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - with pytest.warns(DeprecationWarning): - sentry_init(_experiments={"otel_powered_performance": True}) - mocked_setup_sentry_tracing.assert_called_once() - - -@pytest.mark.forked -def test_integration_not_enabled_if_option_is_off(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - sentry_init(_experiments={"otel_powered_performance": False}) - mocked_setup_sentry_tracing.assert_not_called() - - -@pytest.mark.forked -def test_integration_not_enabled_if_option_is_missing(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - sentry_init() - mocked_setup_sentry_tracing.assert_not_called() - - -@pytest.mark.forked -def test_integration_disabled_with_span_streaming(sentry_init, reset_integrations): - mocked_setup_sentry_tracing = MagicMock() - - with patch( - "sentry_sdk.integrations.opentelemetry.integration._setup_sentry_tracing", - mocked_setup_sentry_tracing, - ): - sentry_init( - integrations=[OpenTelemetryIntegration()], - trace_lifecycle="stream", - ) - mocked_setup_sentry_tracing.assert_not_called() diff --git a/tests/integrations/opentelemetry/test_propagator.py b/tests/integrations/opentelemetry/test_propagator.py deleted file mode 100644 index 1c3e301c3d..0000000000 --- a/tests/integrations/opentelemetry/test_propagator.py +++ /dev/null @@ -1,322 +0,0 @@ -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from opentelemetry import trace -from opentelemetry.context import get_current -from opentelemetry.trace import ( - SpanContext, - TraceFlags, - set_span_in_context, -) -from opentelemetry.trace.propagation import get_current_span - -from sentry_sdk.integrations.opentelemetry.consts import ( - SENTRY_BAGGAGE_KEY, - SENTRY_TRACE_KEY, -) -from sentry_sdk.integrations.opentelemetry.integration import ( - OpenTelemetryIntegration, -) -from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor -from sentry_sdk.scope import global_event_processors -from sentry_sdk.tracing_utils import Baggage - - -@pytest.mark.forked -def test_extract_no_context_no_sentry_trace_header(): - """ - No context and NO Sentry trace data in getter. - Extract should return empty context. - """ - carrier = None - context = None - getter = MagicMock() - getter.get.return_value = None - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert modified_context == {} - - -@pytest.mark.forked -def test_extract_context_no_sentry_trace_header(): - """ - Context but NO Sentry trace data in getter. - Extract should return context as is. - """ - carrier = None - context = {"some": "value"} - getter = MagicMock() - getter.get.return_value = None - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert modified_context == context - - -@pytest.mark.forked -def test_extract_empty_context_sentry_trace_header_no_baggage(): - """ - Empty context but Sentry trace data but NO Baggage in getter. - Extract should return context that has empty baggage in it and also a NoopSpan with span_id and trace_id. - """ - carrier = None - context = {} - getter = MagicMock() - getter.get.side_effect = [ - ["1234567890abcdef1234567890abcdef-1234567890abcdef-1"], - None, - ] - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert len(modified_context.keys()) == 3 - - assert modified_context[SENTRY_TRACE_KEY] == { - "trace_id": "1234567890abcdef1234567890abcdef", - "parent_span_id": "1234567890abcdef", - "parent_sampled": True, - } - assert modified_context[SENTRY_BAGGAGE_KEY].serialize() == "" - - span_context = get_current_span(modified_context).get_span_context() - assert span_context.span_id == int("1234567890abcdef", 16) - assert span_context.trace_id == int("1234567890abcdef1234567890abcdef", 16) - - -@pytest.mark.forked -def test_extract_context_sentry_trace_header_baggage(): - """ - Empty context but Sentry trace data and Baggage in getter. - Extract should return context that has baggage in it and also a NoopSpan with span_id and trace_id. - """ - baggage_header = ( - "other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, " - "sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, " - "sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;" - ) - - carrier = None - context = {"some": "value"} - getter = MagicMock() - getter.get.side_effect = [ - ["1234567890abcdef1234567890abcdef-1234567890abcdef-1"], - [baggage_header], - ] - - modified_context = SentryPropagator().extract(carrier, context, getter) - - assert len(modified_context.keys()) == 4 - - assert modified_context[SENTRY_TRACE_KEY] == { - "trace_id": "1234567890abcdef1234567890abcdef", - "parent_span_id": "1234567890abcdef", - "parent_sampled": True, - } - - assert modified_context[SENTRY_BAGGAGE_KEY].serialize() == ( - "sentry-trace_id=771a43a4192642f0b136d5159a501700," - "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie" - ) - - span_context = get_current_span(modified_context).get_span_context() - assert span_context.span_id == int("1234567890abcdef", 16) - assert span_context.trace_id == int("1234567890abcdef1234567890abcdef", 16) - - -@pytest.mark.forked -def test_inject_empty_otel_span_map(): - """ - Empty otel_span_map. - So there is no sentry_span to be found in inject() - and the function is returned early and no setters are called. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_not_called() - - -@pytest.mark.forked -def test_inject_sentry_span_no_baggage(): - """ - Inject a sentry span with no baggage. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - trace_id = "1234567890abcdef1234567890abcdef" - span_id = "1234567890abcdef" - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - sentry_span = MagicMock() - sentry_span.to_traceparent = mock.Mock( - return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ) - sentry_span.containing_transaction.get_baggage = mock.Mock(return_value=None) - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map[span_id] = sentry_span - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_called_once_with( - carrier, - "sentry-trace", - "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ) - - -def test_inject_sentry_span_empty_baggage(): - """ - Inject a sentry span with no baggage. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - trace_id = "1234567890abcdef1234567890abcdef" - span_id = "1234567890abcdef" - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - sentry_span = MagicMock() - sentry_span.to_traceparent = mock.Mock( - return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ) - sentry_span.containing_transaction.get_baggage = mock.Mock(return_value=Baggage({})) - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map[span_id] = sentry_span - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_called_once_with( - carrier, - "sentry-trace", - "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ) - - -def test_inject_sentry_span_baggage(): - """ - Inject a sentry span with baggage. - """ - carrier = None - context = get_current() - setter = MagicMock() - setter.set = MagicMock() - - trace_id = "1234567890abcdef1234567890abcdef" - span_id = "1234567890abcdef" - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_flags=TraceFlags(TraceFlags.SAMPLED), - is_remote=True, - ) - span = MagicMock() - span.get_span_context.return_value = span_context - - sentry_span = MagicMock() - sentry_span.to_traceparent = mock.Mock( - return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ) - sentry_items = { - "sentry-trace_id": "771a43a4192642f0b136d5159a501700", - "sentry-public_key": "49d0f7386ad645858ae85020e393bef3", - "sentry-sample_rate": 0.01337, - "sentry-user_id": "Amélie", - } - baggage = Baggage(sentry_items=sentry_items) - sentry_span.containing_transaction.get_baggage = MagicMock(return_value=baggage) - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map[span_id] = sentry_span - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span", - return_value=span, - ): - full_context = set_span_in_context(span, context) - SentryPropagator().inject(carrier, full_context, setter) - - setter.set.assert_any_call( - carrier, - "sentry-trace", - "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ) - - setter.set.assert_any_call( - carrier, - "baggage", - baggage.serialize(), - ) - - -def test_inject_no_memory_leak(): - - OpenTelemetryIntegration.setup_once() - - tracer = trace.get_tracer(__name__) - propagator = SentryPropagator() - - cnt_before = len(global_event_processors) - - with tracer.start_as_current_span("bar") as new_span: - context = set_span_in_context(new_span) - carrier = "any_carrier" - propagator.inject(carrier, context) - - cnt_after = len(global_event_processors) - assert cnt_after == cnt_before diff --git a/tests/integrations/opentelemetry/test_span_processor.py b/tests/integrations/opentelemetry/test_span_processor.py deleted file mode 100644 index abecee7301..0000000000 --- a/tests/integrations/opentelemetry/test_span_processor.py +++ /dev/null @@ -1,635 +0,0 @@ -import time -from datetime import datetime, timezone -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from opentelemetry.trace import SpanContext, SpanKind, Status, StatusCode - -import sentry_sdk -from sentry_sdk.integrations.opentelemetry.span_processor import ( - SentrySpanProcessor, - link_trace_context_to_error_event, -) -from sentry_sdk.scope import global_event_processors -from sentry_sdk.tracing import Span, Transaction -from sentry_sdk.tracing_utils import extract_sentrytrace_data -from sentry_sdk.utils import Dsn - - -def test_is_sentry_span(): - otel_span = MagicMock() - - span_processor = SentrySpanProcessor() - assert not span_processor._is_sentry_span(otel_span) - - client = MagicMock() - client.options = {"instrumenter": "otel"} - client.parsed_dsn = Dsn("https://1234567890abcdef@o123456.ingest.sentry.io/123456") - sentry_sdk.get_global_scope().set_client(client) - - assert not span_processor._is_sentry_span(otel_span) - - otel_span.attributes = { - "http.url": "https://example.com", - } - assert not span_processor._is_sentry_span(otel_span) - - otel_span.attributes = { - "http.url": "https://o123456.ingest.sentry.io/api/123/envelope", - } - assert span_processor._is_sentry_span(otel_span) - - -def test_get_otel_context(): - otel_span = MagicMock() - otel_span.attributes = {"foo": "bar"} - otel_span.resource = MagicMock() - otel_span.resource.attributes = {"baz": "qux"} - - span_processor = SentrySpanProcessor() - otel_context = span_processor._get_otel_context(otel_span) - - assert otel_context == { - "attributes": {"foo": "bar"}, - "resource": {"baz": "qux"}, - } - - -def test_get_trace_data_with_span_and_trace(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = None - - parent_context = {} - - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] is None - assert sentry_trace_data["parent_sampled"] is None - assert sentry_trace_data["baggage"] is None - - -def test_get_trace_data_with_span_and_trace_and_parent(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] is None - assert sentry_trace_data["baggage"] is None - - -def test_get_trace_data_with_sentry_trace(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_value", - side_effect=[ - extract_sentrytrace_data( - "1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ), - None, - ], - ): - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] is True - assert sentry_trace_data["baggage"] is None - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_value", - side_effect=[ - extract_sentrytrace_data( - "1234567890abcdef1234567890abcdef-1234567890abcdef-0" - ), - None, - ], - ): - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] is False - assert sentry_trace_data["baggage"] is None - - -def test_get_trace_data_with_sentry_trace_and_baggage(): - otel_span = MagicMock() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - baggage = ( - "sentry-trace_id=771a43a4192642f0b136d5159a501700," - "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie" - ) - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_value", - side_effect=[ - extract_sentrytrace_data( - "1234567890abcdef1234567890abcdef-1234567890abcdef-1" - ), - baggage, - ], - ): - span_processor = SentrySpanProcessor() - sentry_trace_data = span_processor._get_trace_data( - otel_span.get_span_context(), otel_span.parent, parent_context - ) - assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef" - assert sentry_trace_data["span_id"] == "1234567890abcdef" - assert sentry_trace_data["parent_span_id"] == "abcdef1234567890" - assert sentry_trace_data["parent_sampled"] - assert sentry_trace_data["baggage"] == baggage - - -def test_update_span_with_otel_data_http_method(): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.kind = SpanKind.CLIENT - otel_span.attributes = { - "http.method": "GET", - "http.status_code": 429, - "http.status_text": "xxx", - "http.user_agent": "curl/7.64.1", - "net.peer.name": "example.com", - "http.target": "/", - } - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_data(sentry_span, otel_span) - - assert sentry_span.op == "http.client" - assert sentry_span.description == "GET example.com /" - assert sentry_span.status == "resource_exhausted" - - assert sentry_span._data["http.method"] == "GET" - assert sentry_span._data["http.response.status_code"] == 429 - assert sentry_span._data["http.status_text"] == "xxx" - assert sentry_span._data["http.user_agent"] == "curl/7.64.1" - assert sentry_span._data["net.peer.name"] == "example.com" - assert sentry_span._data["http.target"] == "/" - - -@pytest.mark.parametrize( - "otel_status, expected_status", - [ - pytest.param(Status(StatusCode.UNSET), None, id="unset"), - pytest.param(Status(StatusCode.OK), "ok", id="ok"), - pytest.param(Status(StatusCode.ERROR), "internal_error", id="error"), - ], -) -def test_update_span_with_otel_status(otel_status, expected_status): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.kind = SpanKind.INTERNAL - otel_span.status = otel_status - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_status(sentry_span, otel_span) - - assert sentry_span.get_trace_context().get("status") == expected_status - - -def test_update_span_with_otel_data_http_method2(): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.kind = SpanKind.SERVER - otel_span.attributes = { - "http.method": "GET", - "http.status_code": 429, - "http.status_text": "xxx", - "http.user_agent": "curl/7.64.1", - "http.url": "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef", - } - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_data(sentry_span, otel_span) - - assert sentry_span.op == "http.server" - assert sentry_span.description == "GET https://example.com/status/403" - assert sentry_span.status == "resource_exhausted" - - assert sentry_span._data["http.method"] == "GET" - assert sentry_span._data["http.response.status_code"] == 429 - assert sentry_span._data["http.status_text"] == "xxx" - assert sentry_span._data["http.user_agent"] == "curl/7.64.1" - assert ( - sentry_span._data["http.url"] - == "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef" - ) - - -def test_update_span_with_otel_data_db_query(): - sentry_span = Span() - - otel_span = MagicMock() - otel_span.name = "Test OTel Span" - otel_span.attributes = { - "db.system": "postgresql", - "db.statement": "SELECT * FROM table where pwd = '123456'", - } - - span_processor = SentrySpanProcessor() - span_processor._update_span_with_otel_data(sentry_span, otel_span) - - assert sentry_span.op == "db" - assert sentry_span.description == "SELECT * FROM table where pwd = '123456'" - - assert sentry_span._data["db.system"] == "postgresql" - assert ( - sentry_span._data["db.statement"] == "SELECT * FROM table where pwd = '123456'" - ) - - -def test_on_start_transaction(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - fake_start_transaction = MagicMock() - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456" - sentry_sdk.get_global_scope().set_client(fake_client) - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.start_transaction", - fake_start_transaction, - ): - span_processor = SentrySpanProcessor() - span_processor.on_start(otel_span, parent_context) - - fake_start_transaction.assert_called_once_with( - name="Sample OTel Span", - span_id="1234567890abcdef", - parent_span_id="abcdef1234567890", - trace_id="1234567890abcdef1234567890abcdef", - baggage=None, - start_timestamp=datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ), - instrumenter="otel", - origin="auto.otel", - ) - - assert len(span_processor.otel_span_map.keys()) == 1 - assert list(span_processor.otel_span_map.keys())[0] == "1234567890abcdef" - - -def test_on_start_child(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456" - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_span = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map["abcdef1234567890"] = fake_span - span_processor.on_start(otel_span, parent_context) - - fake_span.start_child.assert_called_once_with( - span_id="1234567890abcdef", - name="Sample OTel Span", - start_timestamp=datetime.fromtimestamp( - otel_span.start_time / 1e9, timezone.utc - ), - instrumenter="otel", - origin="auto.otel", - ) - - assert len(span_processor.otel_span_map.keys()) == 2 - assert "abcdef1234567890" in span_processor.otel_span_map.keys() - assert "1234567890abcdef" in span_processor.otel_span_map.keys() - - -def test_on_end_no_sentry_span(): - """ - If on_end is called on a span that is not in the otel_span_map, it should be a no-op. - """ - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.end_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - - span_processor = SentrySpanProcessor() - span_processor.otel_span_map = {} - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - - span_processor.on_end(otel_span) - - span_processor._get_otel_context.assert_not_called() - span_processor._update_span_with_otel_data.assert_not_called() - - -def test_on_end_sentry_transaction(): - """ - Test on_end for a sentry Transaction. - """ - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.end_time = time.time_ns() - otel_span.status = Status(StatusCode.OK) - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_sentry_span = MagicMock(spec=Transaction) - fake_sentry_span.set_context = MagicMock() - fake_sentry_span.finish = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - span_processor.otel_span_map["1234567890abcdef"] = fake_sentry_span - - span_processor.on_end(otel_span) - - fake_sentry_span.set_context.assert_called_once() - span_processor._update_span_with_otel_data.assert_not_called() - fake_sentry_span.set_status.assert_called_once_with("ok") - fake_sentry_span.finish.assert_called_once() - - -def test_on_end_sentry_span(): - """ - Test on_end for a sentry Span. - """ - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.end_time = time.time_ns() - otel_span.status = Status(StatusCode.OK) - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_sentry_span = MagicMock(spec=Span) - fake_sentry_span.set_context = MagicMock() - fake_sentry_span.finish = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - span_processor.otel_span_map["1234567890abcdef"] = fake_sentry_span - - span_processor.on_end(otel_span) - - fake_sentry_span.set_context.assert_not_called() - span_processor._update_span_with_otel_data.assert_called_once_with( - fake_sentry_span, otel_span - ) - fake_sentry_span.set_status.assert_called_once_with("ok") - fake_sentry_span.finish.assert_called_once() - - -def test_link_trace_context_to_error_event(): - """ - Test that the trace context is added to the error event. - """ - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - span_id = "1234567890abcdef" - trace_id = "1234567890abcdef1234567890abcdef" - - fake_trace_context = { - "bla": "blub", - "foo": "bar", - "baz": 123, - } - - sentry_span = MagicMock() - sentry_span.get_trace_context = MagicMock(return_value=fake_trace_context) - - otel_span_map = { - span_id: sentry_span, - } - - span_context = SpanContext( - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - is_remote=True, - ) - otel_span = MagicMock() - otel_span.get_span_context = MagicMock(return_value=span_context) - - fake_event = {"event_id": "1234567890abcdef1234567890abcdef"} - - with mock.patch( - "sentry_sdk.integrations.opentelemetry.span_processor.get_current_span", - return_value=otel_span, - ): - event = link_trace_context_to_error_event(fake_event, otel_span_map) - - assert event - assert event == fake_event # the event is changed in place inside the function - assert "contexts" in event - assert "trace" in event["contexts"] - assert event["contexts"]["trace"] == fake_trace_context - - -def test_pruning_old_spans_on_start(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - parent_context = {} - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel", "debug": False} - fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456" - sentry_sdk.get_global_scope().set_client(fake_client) - - span_processor = SentrySpanProcessor() - - span_processor.otel_span_map = { - "111111111abcdef": MagicMock(), # should stay - "2222222222abcdef": MagicMock(), # should go - "3333333333abcdef": MagicMock(), # should go - } - current_time_minutes = int(time.time() / 60) - span_processor.open_spans = { - current_time_minutes - 3: {"111111111abcdef"}, # should stay - current_time_minutes - 11: { - "2222222222abcdef", - "3333333333abcdef", - }, # should go - } - - span_processor.on_start(otel_span, parent_context) - assert sorted(list(span_processor.otel_span_map.keys())) == [ - "111111111abcdef", - "1234567890abcdef", - ] - assert sorted(list(span_processor.open_spans.values())) == [ - {"111111111abcdef"}, - {"1234567890abcdef"}, - ] - - -def test_pruning_old_spans_on_end(): - otel_span = MagicMock() - otel_span.name = "Sample OTel Span" - otel_span.start_time = time.time_ns() - span_context = SpanContext( - trace_id=int("1234567890abcdef1234567890abcdef", 16), - span_id=int("1234567890abcdef", 16), - is_remote=True, - ) - otel_span.get_span_context.return_value = span_context - otel_span.parent = MagicMock() - otel_span.parent.span_id = int("abcdef1234567890", 16) - - fake_client = MagicMock() - fake_client.options = {"instrumenter": "otel"} - sentry_sdk.get_global_scope().set_client(fake_client) - - fake_sentry_span = MagicMock(spec=Span) - fake_sentry_span.set_context = MagicMock() - fake_sentry_span.finish = MagicMock() - - span_processor = SentrySpanProcessor() - span_processor._get_otel_context = MagicMock() - span_processor._update_span_with_otel_data = MagicMock() - - span_processor.otel_span_map = { - "111111111abcdef": MagicMock(), # should stay - "2222222222abcdef": MagicMock(), # should go - "3333333333abcdef": MagicMock(), # should go - "1234567890abcdef": fake_sentry_span, # should go (because it is closed) - } - current_time_minutes = int(time.time() / 60) - span_processor.open_spans = { - current_time_minutes: {"1234567890abcdef"}, # should go (because it is closed) - current_time_minutes - 3: {"111111111abcdef"}, # should stay - current_time_minutes - 11: { - "2222222222abcdef", - "3333333333abcdef", - }, # should go - } - - span_processor.on_end(otel_span) - assert sorted(list(span_processor.otel_span_map.keys())) == ["111111111abcdef"] - assert sorted(list(span_processor.open_spans.values())) == [{"111111111abcdef"}] - - -def test_no_memory_leak(): - span_processor_1 = SentrySpanProcessor() - cnt_before = len(global_event_processors) - - span_processor_2 = SentrySpanProcessor() - - cnt_after = len(global_event_processors) - assert span_processor_1 is span_processor_2 - assert cnt_before == cnt_after diff --git a/tests/integrations/otlp/test_otlp.py b/tests/integrations/otlp/test_otlp.py index 25e2a5f7fe..504507e274 100644 --- a/tests/integrations/otlp/test_otlp.py +++ b/tests/integrations/otlp/test_otlp.py @@ -16,7 +16,10 @@ ) from opentelemetry.util._once import Once -from sentry_sdk.integrations.otlp import OTLPIntegration, SentryOTLPPropagator +from sentry_sdk.integrations.otlp import ( + OTLPIntegration, + SentryOTLPPropagator, +) from sentry_sdk.scope import get_external_propagation_context original_propagator = get_global_textmap() diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 286d6aeaff..59c947615f 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -1,8 +1,6 @@ import importlib -import json import sys import threading -from unittest import mock import pytest @@ -589,82 +587,6 @@ async def dispatch_request(self): assert event["transaction"] == "hello_class" -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@pytest.mark.asyncio -async def test_active_thread_id( - sentry_init, capture_envelopes, teardown_profiling, endpoint -): - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) - app = quart_app_factory() - - envelopes = capture_envelopes() - - async with app.test_client() as client: - response = await client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(await response.get_data(as_text=True)) - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1, envelopes[0].items - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [ - item for item in envelopes[0].items if item.type == "transaction" - ] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@pytest.mark.asyncio -async def test_active_thread_id_span_streaming( - sentry_init, capture_items, teardown_profiling, endpoint -): - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - trace_lifecycle="stream", - ) - app = quart_app_factory() - - items = capture_items("span") - - async with app.test_client() as client: - response = await client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(await response.get_data(as_text=True)) - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 1 - - segment = spans[0] - assert str(data["active"]) == segment["attributes"]["thread.id"] - - @pytest.mark.asyncio async def test_span_origin(sentry_init, capture_events): sentry_init( diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index bf0d7b0993..2ef4a7ab57 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -7,7 +7,6 @@ import re import threading import warnings -from unittest import mock import pytest import starlette @@ -1453,44 +1452,6 @@ def test_legacy_setup( assert event["transaction"] == "/message/{message_id}" -@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint): - sentry_init( - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) - app = starlette_app_factory() - asgi_app = SentryAsgiMiddleware(app) - - envelopes = capture_envelopes() - - client = TestClient(asgi_app) - response = client.get(endpoint) - assert response.status_code == 200 - - data = json.loads(response.content) - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] - - transactions = [item for item in envelopes[0].items if item.type == "transaction"] - assert len(transactions) == 1 - - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] - - @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): sentry_init( diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index 9c06ce24a7..70a4d47e37 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -38,77 +38,6 @@ def crash(): assert not events -@pytest.mark.filterwarnings("ignore:.*:pytest.PytestUnhandledThreadExceptionWarning") -@pytest.mark.parametrize("propagate_hub", (True, False)) -def test_propagates_hub(sentry_init, capture_events, propagate_hub): - sentry_init( - default_integrations=False, - integrations=[ThreadingIntegration(propagate_hub=propagate_hub)], - ) - events = capture_events() - - def stage1(): - sentry_sdk.get_isolation_scope().set_tag("stage1", "true") - - t = Thread(target=stage2) - t.start() - t.join() - - def stage2(): - 1 / 0 - - t = Thread(target=stage1) - t.start() - t.join() - - (event,) = events - - (exception,) = event["exception"]["values"] - - assert exception["type"] == "ZeroDivisionError" - assert exception["mechanism"]["type"] == "threading" - assert not exception["mechanism"]["handled"] - - # Free-threaded builds set thread_inherit_context to True, otherwise thread_inherit_context is False - if propagate_hub or getattr(sys.flags, "thread_inherit_context", None): - assert event["tags"]["stage1"] == "true" - else: - assert "stage1" not in event.get("tags", {}) - - -@pytest.mark.parametrize("propagate_hub", (True, False)) -def test_propagates_threadpool_hub(sentry_init, capture_events, propagate_hub): - sentry_init( - traces_sample_rate=1.0, - integrations=[ThreadingIntegration(propagate_hub=propagate_hub)], - ) - events = capture_events() - - def double(number): - with sentry_sdk.start_span(op="task", name=str(number)): - return number * 2 - - with sentry_sdk.start_transaction(name="test_handles_threadpool"): - with futures.ThreadPoolExecutor(max_workers=1) as executor: - tasks = [executor.submit(double, number) for number in [1, 2, 3, 4]] - for future in futures.as_completed(tasks): - print("Getting future value!", future.result()) - - sentry_sdk.flush() - - # Free-threaded builds set thread_inherit_context to True, otherwise thread_inherit_context is False - if propagate_hub or getattr(sys.flags, "thread_inherit_context", None): - assert len(events) == 1 - (event,) = events - assert event["spans"][0]["trace_id"] == event["spans"][1]["trace_id"] - assert event["spans"][1]["trace_id"] == event["spans"][2]["trace_id"] - assert event["spans"][2]["trace_id"] == event["spans"][3]["trace_id"] - assert event["spans"][3]["trace_id"] == event["spans"][0]["trace_id"] - else: - (event,) = events - assert len(event["spans"]) == 0 - - @pytest.mark.skip(reason="Temporarily disable to release SDK 2.0a1.") def test_circular_references(sentry_init, request): sentry_init(default_integrations=False, integrations=[ThreadingIntegration()]) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index eb73be1499..ca7761a7b4 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -614,33 +614,6 @@ def sample_app(environ, start_response): assert sum(agg.get("crashed", 0) for agg in session_aggregates) == 1 -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_profile_sent( - sentry_init, - capture_envelopes, - teardown_profiling, -): - def test_app(environ, start_response): - start_response("200 OK", []) - return ["Go get the ball! Good dog!"] - - sentry_init( - traces_sample_rate=1.0, - _experiments={"profiles_sample_rate": 1.0}, - ) - app = SentryWsgiMiddleware(test_app) - envelopes = capture_envelopes() - - client = Client(app) - client.get("/") - - envelopes = [envelope for envelope in envelopes] - assert len(envelopes) == 1 - - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 - - @pytest.mark.parametrize("span_streaming", [True, False]) def test_span_origin_manual(sentry_init, capture_events, capture_items, span_streaming): def dogpark(environ, start_response): diff --git a/tests/new_scopes_compat/__init__.py b/tests/new_scopes_compat/__init__.py deleted file mode 100644 index 45391bd9ad..0000000000 --- a/tests/new_scopes_compat/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Separate module for tests that check backwards compatibility of the Hub API with 1.x. -These tests should be removed once we remove the Hub API, likely in the next major. - -All tests in this module are run with hub isolation, provided by `isolate_hub` autouse -fixture, defined in `conftest.py`. -""" diff --git a/tests/new_scopes_compat/conftest.py b/tests/new_scopes_compat/conftest.py deleted file mode 100644 index 7023b9a63f..0000000000 --- a/tests/new_scopes_compat/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest - -import sentry_sdk - - -@pytest.fixture(autouse=True) -def isolate_hub(suppress_deprecation_warnings): - with sentry_sdk.Hub(None): - yield diff --git a/tests/new_scopes_compat/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py deleted file mode 100644 index 21e2ac27d3..0000000000 --- a/tests/new_scopes_compat/test_new_scopes_compat.py +++ /dev/null @@ -1,275 +0,0 @@ -import sentry_sdk -from sentry_sdk.hub import Hub - -""" -Those tests are meant to check the compatibility of the new scopes in SDK 2.0 with the old Hub/Scope system in SDK 1.x. - -Those tests have been run with the latest SDK 1.x versiona and the data used in the `assert` statements represents -the behvaior of the SDK 1.x. - -This makes sure that we are backwards compatible. (on a best effort basis, there will probably be some edge cases that are not covered here) -""" - - -def test_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with configure_scope` block. - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with sentry_sdk.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B1", 1) - scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} - - -def test_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with sentry_sdk.push_scope() as scope: # push scope - sentry_sdk.set_tag("B1", 1) - scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} - - -def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == { - "A": 1, - "B1": 1, - "B2": 1, - "B3": 1, - "B4": 1, - "B5": 1, - "Z": 1, - } - - -def test_with_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B5": 1, "Z": 1} - - -def test_with_cloned_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_cloned_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} diff --git a/tests/new_scopes_compat/test_new_scopes_compat_event.py b/tests/new_scopes_compat/test_new_scopes_compat_event.py deleted file mode 100644 index 0b8a4ecba9..0000000000 --- a/tests/new_scopes_compat/test_new_scopes_compat_event.py +++ /dev/null @@ -1,498 +0,0 @@ -from unittest import mock - -import pytest - -import sentry_sdk -from sentry_sdk.hub import Hub -from sentry_sdk.integrations import iter_default_integrations -from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber - -""" -Those tests are meant to check the compatibility of the new scopes in SDK 2.0 with the old Hub/Scope system in SDK 1.x. - -Those tests have been run with the latest SDK 1.x version and the data used in the `assert` statements represents -the behvaior of the SDK 1.x. - -This makes sure that we are backwards compatible. (on a best effort basis, there will probably be some edge cases that are not covered here) -""" - - -@pytest.fixture -def integrations(): - return [ - integration.identifier - for integration in iter_default_integrations( - with_auto_enabling_integrations=False - ) - ] - - -@pytest.fixture -def expected_error(integrations): - def create_expected_error_event(trx, span): - return { - "level": "warning-X", - "exception": { - "values": [ - { - "mechanism": {"type": "generic", "handled": True}, - "module": None, - "type": "ValueError", - "value": "This is a test exception", - "stacktrace": { - "frames": [ - { - "filename": "tests/new_scopes_compat/test_new_scopes_compat_event.py", - "abs_path": mock.ANY, - "function": "_faulty_function", - "module": "tests.new_scopes_compat.test_new_scopes_compat_event", - "lineno": mock.ANY, - "pre_context": [ - " return create_expected_transaction_event", - "", - "", - "def _faulty_function():", - " try:", - ], - "context_line": ' raise ValueError("This is a test exception")', - "post_context": [ - " except ValueError as ex:", - " sentry_sdk.capture_exception(ex)", - "", - "", - "def _test_before_send(event, hint):", - ], - "vars": { - "ex": mock.ANY, - }, - "in_app": True, - } - ] - }, - } - ] - }, - "event_id": mock.ANY, - "timestamp": mock.ANY, - "contexts": { - "character": { - "name": "Mighty Fighter changed by before_send", - "age": 19, - "attack_type": "melee", - }, - "trace": { - "trace_id": trx.trace_id, - "span_id": span.span_id, - "parent_span_id": span.parent_span_id, - "op": "test_span", - "origin": "manual", - "description": None, - "data": { - "thread.id": mock.ANY, - "thread.name": "MainThread", - }, - }, - "runtime": { - "name": "CPython", - "version": mock.ANY, - "build": mock.ANY, - }, - }, - "user": { - "id": "123", - "email": "jane.doe@example.com", - }, - "transaction": "test_transaction", - "transaction_info": {"source": "custom"}, - "tags": {"tag1": "tag1_value", "tag2": "tag2_value"}, - "extra": { - "extra1": "extra1_value", - "extra2": "extra2_value", - "should_be_removed_by_event_scrubber": "[Filtered]", - "sys.argv": "[Filtered]", - }, - "breadcrumbs": { - "values": [ - { - "category": "error-level", - "message": "Authenticated user %s", - "level": "error", - "data": {"breadcrumb2": "somedata"}, - "timestamp": mock.ANY, - "type": "default", - } - ] - }, - "modules": mock.ANY, - "release": "0.1.2rc3", - "environment": "checking-compatibility-with-sdk1", - "server_name": mock.ANY, - "sdk": { - "name": "sentry.python", - "version": mock.ANY, - "packages": [{"name": "pypi:sentry-sdk", "version": mock.ANY}], - "integrations": integrations, - }, - "platform": "python", - "_meta": { - "extra": { - "should_be_removed_by_event_scrubber": { - "": {"rem": [["!config", "s"]]} - }, - "sys.argv": {"": {"rem": [["!config", "s"]]}}, - }, - }, - } - - return create_expected_error_event - - -@pytest.fixture -def expected_transaction(integrations): - def create_expected_transaction_event(trx, span): - return { - "type": "transaction", - "transaction": "test_transaction changed by before_send_transaction", - "transaction_info": {"source": "custom"}, - "contexts": { - "trace": { - "trace_id": trx.trace_id, - "span_id": trx.span_id, - "parent_span_id": None, - "op": "test_transaction_op", - "origin": "manual", - "description": None, - "data": { - "thread.id": mock.ANY, - "thread.name": "MainThread", - }, - }, - "character": { - "name": "Mighty Fighter changed by before_send_transaction", - "age": 19, - "attack_type": "melee", - }, - "runtime": { - "name": "CPython", - "version": mock.ANY, - "build": mock.ANY, - }, - }, - "tags": {"tag1": "tag1_value", "tag2": "tag2_value"}, - "timestamp": mock.ANY, - "start_timestamp": mock.ANY, - "spans": [ - { - "data": { - "thread.id": mock.ANY, - "thread.name": "MainThread", - }, - "trace_id": trx.trace_id, - "span_id": span.span_id, - "parent_span_id": span.parent_span_id, - "same_process_as_parent": True, - "op": "test_span", - "origin": "manual", - "description": None, - "start_timestamp": mock.ANY, - "timestamp": mock.ANY, - } - ], - "measurements": {"memory_used": {"value": 456, "unit": "byte"}}, - "event_id": mock.ANY, - "level": "warning-X", - "user": { - "id": "123", - "email": "jane.doe@example.com", - }, - "extra": { - "extra1": "extra1_value", - "extra2": "extra2_value", - "should_be_removed_by_event_scrubber": "[Filtered]", - "sys.argv": "[Filtered]", - }, - "release": "0.1.2rc3", - "environment": "checking-compatibility-with-sdk1", - "server_name": mock.ANY, - "sdk": { - "name": "sentry.python", - "version": mock.ANY, - "packages": [{"name": "pypi:sentry-sdk", "version": mock.ANY}], - "integrations": integrations, - }, - "platform": "python", - "_meta": { - "extra": { - "should_be_removed_by_event_scrubber": { - "": {"rem": [["!config", "s"]]} - }, - "sys.argv": {"": {"rem": [["!config", "s"]]}}, - }, - }, - } - - return create_expected_transaction_event - - -def _faulty_function(): - try: - raise ValueError("This is a test exception") - except ValueError as ex: - sentry_sdk.capture_exception(ex) - - -def _test_before_send(event, hint): - event["contexts"]["character"]["name"] += " changed by before_send" - return event - - -def _test_before_send_transaction(event, hint): - event["transaction"] += " changed by before_send_transaction" - event["contexts"]["character"]["name"] += " changed by before_send_transaction" - return event - - -def _test_before_breadcrumb(breadcrumb, hint): - if breadcrumb["category"] == "info-level": - return None - return breadcrumb - - -def _generate_event_data(scope=None): - """ - Generates some data to be used in the events sent by the tests. - """ - sentry_sdk.set_level("warning-X") - - sentry_sdk.add_breadcrumb( - category="info-level", - message="Authenticated user %s", - level="info", - data={"breadcrumb1": "somedata"}, - ) - sentry_sdk.add_breadcrumb( - category="error-level", - message="Authenticated user %s", - level="error", - data={"breadcrumb2": "somedata"}, - ) - - sentry_sdk.set_context( - "character", - { - "name": "Mighty Fighter", - "age": 19, - "attack_type": "melee", - }, - ) - - sentry_sdk.set_extra("extra1", "extra1_value") - sentry_sdk.set_extra("extra2", "extra2_value") - sentry_sdk.set_extra("should_be_removed_by_event_scrubber", "XXX") - - sentry_sdk.set_tag("tag1", "tag1_value") - sentry_sdk.set_tag("tag2", "tag2_value") - - sentry_sdk.set_user( - {"id": "123", "email": "jane.doe@example.com", "ip_address": "211.161.1.124"} - ) - - sentry_sdk.set_measurement("memory_used", 456, "byte") - - if scope is not None: - scope.add_attachment(bytes=b"Hello World", filename="hello.txt") - - -def _init_sentry_sdk(sentry_init): - sentry_init( - environment="checking-compatibility-with-sdk1", - release="0.1.2rc3", - before_send=_test_before_send, - before_send_transaction=_test_before_send_transaction, - before_breadcrumb=_test_before_breadcrumb, - event_scrubber=EventScrubber( - denylist=DEFAULT_DENYLIST - + ["should_be_removed_by_event_scrubber", "sys.argv"] - ), - send_default_pii=False, - traces_sample_rate=1.0, - auto_enabling_integrations=False, - ) - - -# -# The actual Tests start here! -# - - -def test_event(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - with sentry_sdk.configure_scope() as scope: # configure scope - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event2(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub.current: # with hub - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - with sentry_sdk.configure_scope() as scope: # configure scope - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event3(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub.current: # with hub - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.push_scope() as scope: # push scope - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event4(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub(Hub.current): # with hub clone - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - with sentry_sdk.configure_scope() as scope: # configure scope - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" - - -def test_event5(sentry_init, capture_envelopes, expected_error, expected_transaction): - _init_sentry_sdk(sentry_init) - - envelopes = capture_envelopes() - - with Hub(Hub.current): - sentry_sdk.set_tag("A", 1) # will not be added - - with Hub(Hub.current): # with hub clone - with sentry_sdk.push_scope() as scope: - scope.set_tag("B", 1) # will not be added - - with sentry_sdk.push_scope() as scope: # push scope - with sentry_sdk.start_transaction( - name="test_transaction", op="test_transaction_op" - ) as trx: - with sentry_sdk.start_span(op="test_span") as span: - _generate_event_data(scope) - _faulty_function() - - (error_envelope, transaction_envelope) = envelopes - - error = error_envelope.get_event() - transaction = transaction_envelope.get_transaction_event() - attachment = error_envelope.items[-1] - - assert error == expected_error(trx, span) - assert transaction == expected_transaction(trx, span) - assert attachment.headers == { - "filename": "hello.txt", - "type": "attachment", - "content_type": "text/plain", - } - assert attachment.payload.bytes == b"Hello World" diff --git a/tests/profiler/test_transaction_profiler.py b/tests/profiler/test_transaction_profiler.py deleted file mode 100644 index 749e91add0..0000000000 --- a/tests/profiler/test_transaction_profiler.py +++ /dev/null @@ -1,838 +0,0 @@ -import inspect -import os -import sys -import threading -import time -import warnings -from collections import defaultdict -from unittest import mock - -import pytest - -import sentry_sdk -from sentry_sdk import start_transaction -from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.profiler.transaction_profiler import ( - GeventScheduler, - Profile, - Scheduler, - ThreadScheduler, - setup_profiler, -) -from sentry_sdk.profiler.utils import ( - extract_frame, - extract_stack, - frame_id, - get_frame_name, -) - -try: - import gevent -except ImportError: - gevent = None - - -requires_gevent = pytest.mark.skipif(gevent is None, reason="gevent not enabled") - - -def process_test_sample(sample): - # insert a mock hashable for the stack - return [(tid, (stack, stack)) for tid, stack in sample] - - -def non_experimental_options(mode=None, sample_rate=None): - return {"profiler_mode": mode, "profiles_sample_rate": sample_rate} - - -def experimental_options(mode=None, sample_rate=None): - return { - "_experiments": {"profiler_mode": mode, "profiles_sample_rate": sample_rate} - } - - -@pytest.mark.parametrize( - "mode", - [pytest.param("foo")], -) -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -def test_profiler_invalid_mode(mode, make_options, teardown_profiling): - with pytest.raises(ValueError): - setup_profiler(make_options(mode)) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("sleep"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -def test_profiler_valid_mode(mode, make_options, teardown_profiling): - # should not raise any exceptions - setup_profiler(make_options(mode)) - - -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -def test_profiler_setup_twice(make_options, teardown_profiling): - # setting up the first time should return True to indicate success - assert setup_profiler(make_options()) - # setting up the second time should return False to indicate no-op - assert not setup_profiler(make_options()) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@pytest.mark.parametrize( - ("profiles_sample_rate", "profile_count"), - [ - pytest.param(1.00, 1, id="profiler sampled at 1.00"), - pytest.param(0.75, 1, id="profiler sampled at 0.75"), - pytest.param(0.25, 0, id="profiler sampled at 0.25"), - pytest.param(0.00, 0, id="profiler sampled at 0.00"), - pytest.param(None, 0, id="profiler not enabled"), - ], -) -@pytest.mark.parametrize( - "make_options", - [ - pytest.param(experimental_options, id="experiment"), - pytest.param(non_experimental_options, id="non experimental"), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_profiles_sample_rate( - sentry_init, - capture_envelopes, - capture_record_lost_event_calls, - teardown_profiling, - profiles_sample_rate, - profile_count, - make_options, - mode, -): - options = make_options(mode=mode, sample_rate=profiles_sample_rate) - sentry_init( - traces_sample_rate=1.0, - profiler_mode=options.get("profiler_mode"), - profiles_sample_rate=options.get("profiles_sample_rate"), - _experiments=options.get("_experiments", {}), - ) - - envelopes = capture_envelopes() - record_lost_event_calls = capture_record_lost_event_calls() - - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.random.random", return_value=0.5 - ): - with start_transaction(name="profiling"): - pass - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - assert len(items["profile"]) == profile_count - if profiles_sample_rate is None or profiles_sample_rate == 0: - assert record_lost_event_calls == [] - elif profile_count: - assert record_lost_event_calls == [] - else: - assert record_lost_event_calls == [("sample_rate", "profile", None, 1)] - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@pytest.mark.parametrize( - ("profiles_sampler", "profile_count"), - [ - pytest.param(lambda _: 1.00, 1, id="profiler sampled at 1.00"), - pytest.param(lambda _: 0.75, 1, id="profiler sampled at 0.75"), - pytest.param(lambda _: 0.25, 0, id="profiler sampled at 0.25"), - pytest.param(lambda _: 0.00, 0, id="profiler sampled at 0.00"), - pytest.param(lambda _: None, 0, id="profiler not enabled"), - pytest.param( - lambda ctx: 1 if ctx["transaction_context"]["name"] == "profiling" else 0, - 1, - id="profiler sampled for transaction name", - ), - pytest.param( - lambda ctx: 0 if ctx["transaction_context"]["name"] == "profiling" else 1, - 0, - id="profiler not sampled for transaction name", - ), - pytest.param( - lambda _: "1", 0, id="profiler not sampled because string sample rate" - ), - pytest.param(lambda _: True, 1, id="profiler sampled at True"), - pytest.param(lambda _: False, 0, id="profiler sampled at False"), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0) -def test_profiles_sampler( - sentry_init, - capture_envelopes, - capture_record_lost_event_calls, - teardown_profiling, - profiles_sampler, - profile_count, - mode, -): - sentry_init( - traces_sample_rate=1.0, - profiles_sampler=profiles_sampler, - ) - - envelopes = capture_envelopes() - record_lost_event_calls = capture_record_lost_event_calls() - - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.random.random", return_value=0.5 - ): - with start_transaction(name="profiling"): - pass - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - assert len(items["profile"]) == profile_count - if profile_count: - assert record_lost_event_calls == [] - else: - assert record_lost_event_calls == [("sample_rate", "profile", None, 1)] - - -def test_minimum_unique_samples_required( - sentry_init, - capture_envelopes, - capture_record_lost_event_calls, - teardown_profiling, -): - sentry_init( - traces_sample_rate=1.0, - _experiments={"profiles_sample_rate": 1.0}, - ) - - envelopes = capture_envelopes() - record_lost_event_calls = capture_record_lost_event_calls() - - with start_transaction(name="profiling"): - pass - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - # because we dont leave any time for the profiler to - # take any samples, it should be not be sent - assert len(items["profile"]) == 0 - assert record_lost_event_calls == [("insufficient_data", "profile", None, 1)] - - -@pytest.mark.forked -def test_profile_captured( - sentry_init, - capture_envelopes, - teardown_profiling, -): - sentry_init( - traces_sample_rate=1.0, - _experiments={"profiles_sample_rate": 1.0}, - ) - - envelopes = capture_envelopes() - - with start_transaction(name="profiling"): - time.sleep(0.05) - - items = defaultdict(list) - for envelope in envelopes: - for item in envelope.items: - items[item.type].append(item) - - assert len(items["transaction"]) == 1 - assert len(items["profile"]) == 1 - - -def get_frame(depth=1): - """ - This function is not exactly true to its name. Depending on - how it is called, the true depth of the stack can be deeper - than the argument implies. - """ - if depth <= 0: - raise ValueError("only positive integers allowed") - if depth > 1: - return get_frame(depth=depth - 1) - return inspect.currentframe() - - -class GetFrameBase: - def inherited_instance_method(self): - return inspect.currentframe() - - def inherited_instance_method_wrapped(self): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @classmethod - def inherited_class_method(cls): - return inspect.currentframe() - - @classmethod - def inherited_class_method_wrapped(cls): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @staticmethod - def inherited_static_method(): - return inspect.currentframe() - - -class GetFrame(GetFrameBase): - def instance_method(self): - return inspect.currentframe() - - def instance_method_wrapped(self): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @classmethod - def class_method(cls): - return inspect.currentframe() - - @classmethod - def class_method_wrapped(cls): - def wrapped(): - return inspect.currentframe() - - return wrapped - - @staticmethod - def static_method(): - return inspect.currentframe() - - -@pytest.mark.parametrize( - ("frame", "frame_name"), - [ - pytest.param( - get_frame(), - "get_frame", - id="function", - ), - pytest.param( - (lambda: inspect.currentframe())(), - "", - id="lambda", - ), - pytest.param( - GetFrame().instance_method(), - "GetFrame.instance_method", - id="instance_method", - ), - pytest.param( - GetFrame().instance_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrame.instance_method_wrapped..wrapped" - ), - id="instance_method_wrapped", - ), - pytest.param( - GetFrame().class_method(), - "GetFrame.class_method", - id="class_method", - ), - pytest.param( - GetFrame().class_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrame.class_method_wrapped..wrapped" - ), - id="class_method_wrapped", - ), - pytest.param( - GetFrame().static_method(), - "static_method" if sys.version_info < (3, 11) else "GetFrame.static_method", - id="static_method", - ), - pytest.param( - GetFrame().inherited_instance_method(), - "GetFrameBase.inherited_instance_method", - id="inherited_instance_method", - ), - pytest.param( - GetFrame().inherited_instance_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_instance_method_wrapped..wrapped" - ), - id="instance_method_wrapped", - ), - pytest.param( - GetFrame().inherited_class_method(), - "GetFrameBase.inherited_class_method", - id="inherited_class_method", - ), - pytest.param( - GetFrame().inherited_class_method_wrapped()(), - ( - "wrapped" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_class_method_wrapped..wrapped" - ), - id="inherited_class_method_wrapped", - ), - pytest.param( - GetFrame().inherited_static_method(), - ( - "inherited_static_method" - if sys.version_info < (3, 11) - else "GetFrameBase.inherited_static_method" - ), - id="inherited_static_method", - ), - ], -) -def test_get_frame_name(frame, frame_name): - assert get_frame_name(frame) == frame_name - - -@pytest.mark.parametrize( - ("get_frame", "function"), - [ - pytest.param(lambda: get_frame(depth=1), "get_frame", id="simple"), - ], -) -def test_extract_frame(get_frame, function): - cwd = os.getcwd() - frame = get_frame() - extracted_frame = extract_frame(frame_id(frame), frame, cwd) - - # the abs_path should be equal toe the normalized path of the co_filename - assert extracted_frame["abs_path"] == os.path.normpath(frame.f_code.co_filename) - - # the module should be pull from this test module - assert extracted_frame["module"] == __name__ - - # the filename should be the file starting after the cwd - assert extracted_frame["filename"] == __file__[len(cwd) + 1 :] - - assert extracted_frame["function"] == function - - # the lineno will shift over time as this file is modified so just check - # that it is an int - assert isinstance(extracted_frame["lineno"], int) - - -@pytest.mark.parametrize( - ("depth", "max_stack_depth", "actual_depth"), - [ - pytest.param(1, 128, 1, id="less than"), - pytest.param(256, 128, 128, id="greater than"), - pytest.param(128, 128, 128, id="equals"), - ], -) -def test_extract_stack_with_max_depth(depth, max_stack_depth, actual_depth): - # introduce a lambda that we'll be looking for in the stack - frame = (lambda: get_frame(depth=depth))() - - # plus 1 because we introduced a lambda intentionally that we'll - # look for in the final stack to make sure its in the right position - base_stack_depth = len(inspect.stack()) + 1 - - # increase the max_depth by the `base_stack_depth` to account - # for the extra frames pytest will add - _, frame_ids, frames = extract_stack( - frame, - LRUCache(max_size=1), - max_stack_depth=max_stack_depth + base_stack_depth, - cwd=os.getcwd(), - ) - assert len(frame_ids) == base_stack_depth + actual_depth - assert len(frames) == base_stack_depth + actual_depth - - for i in range(actual_depth): - assert frames[i]["function"] == "get_frame", i - - # index 0 contains the inner most frame on the stack, so the lamdba - # should be at index `actual_depth` - if sys.version_info >= (3, 11): - assert ( - frames[actual_depth]["function"] - == "test_extract_stack_with_max_depth.." - ), actual_depth - else: - assert frames[actual_depth]["function"] == "", actual_depth - - -@pytest.mark.parametrize( - ("frame", "depth"), - [(get_frame(depth=1), len(inspect.stack()))], -) -def test_extract_stack_with_cache(frame, depth): - # make sure cache has enough room or this test will fail - cache = LRUCache(max_size=depth) - cwd = os.getcwd() - _, _, frames1 = extract_stack(frame, cache, cwd=cwd) - _, _, frames2 = extract_stack(frame, cache, cwd=cwd) - - assert len(frames1) > 0 - assert len(frames2) > 0 - assert len(frames1) == len(frames2) - for i, (frame1, frame2) in enumerate(zip(frames1, frames2)): - # DO NOT use `==` for the assertion here since we are - # testing for identity, and using `==` would test for - # equality which would always pass since we're extract - # the same stack. - assert frame1 is frame2, i - - -def get_scheduler_threads(scheduler): - return [thread for thread in threading.enumerate() if thread.name == scheduler.name] - - -@pytest.mark.parametrize( - ("scheduler_class",), - [ - pytest.param(ThreadScheduler, id="thread scheduler"), - pytest.param( - GeventScheduler, - marks=[ - requires_gevent, - pytest.mark.skip( - reason="cannot find this thread via threading.enumerate()" - ), - ], - id="gevent scheduler", - ), - ], -) -def test_thread_scheduler_single_background_thread(scheduler_class): - scheduler = scheduler_class(frequency=1000) - - # not yet setup, no scheduler threads yet - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.setup() - - # setup but no profiles started so still no threads - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.ensure_running() - - # the scheduler will start always 1 thread - assert len(get_scheduler_threads(scheduler)) == 1 - - scheduler.ensure_running() - - # the scheduler still only has 1 thread - assert len(get_scheduler_threads(scheduler)) == 1 - - scheduler.teardown() - - # once finished, the thread should stop - assert len(get_scheduler_threads(scheduler)) == 0 - - -@pytest.mark.parametrize( - ("scheduler_class",), - [ - pytest.param(ThreadScheduler, id="thread scheduler"), - pytest.param( - GeventScheduler, - marks=[ - requires_gevent, - pytest.mark.skip( - reason="cannot find this thread via threading.enumerate()" - ), - ], - id="gevent scheduler", - ), - ], -) -def test_thread_scheduler_no_thread_on_shutdown(scheduler_class): - scheduler = scheduler_class(frequency=1000) - - # not yet setup, no scheduler threads yet - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.setup() - - # setup but no profiles started so still no threads - assert len(get_scheduler_threads(scheduler)) == 0 - - # mock RuntimeError as if the 3.12 interpreter was shutting down - with mock.patch( - "threading.Thread.start", - side_effect=RuntimeError("can't create new thread at interpreter shutdown"), - ): - scheduler.ensure_running() - - assert scheduler.running is False - - # still no thread - assert len(get_scheduler_threads(scheduler)) == 0 - - scheduler.teardown() - - assert len(get_scheduler_threads(scheduler)) == 0 - - -@pytest.mark.parametrize( - ("scheduler_class",), - [ - pytest.param(ThreadScheduler, id="thread scheduler"), - pytest.param(GeventScheduler, marks=requires_gevent, id="gevent scheduler"), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.MAX_PROFILE_DURATION_NS", 1) -def test_max_profile_duration_reached(scheduler_class): - sample = [ - ( - "1", - extract_stack( - get_frame(), - LRUCache(max_size=1), - cwd=os.getcwd(), - ), - ), - ] - - with scheduler_class(frequency=1000) as scheduler: - with Profile(True, 0, scheduler=scheduler) as profile: - # profile just started, it's active - assert profile.active - - # write a sample at the start time, so still active - profile.write(profile.start_ns + 0, sample) - assert profile.active - - # write a sample at max time, so still active - profile.write(profile.start_ns + 1, sample) - assert profile.active - - # write a sample PAST the max time, so now inactive - profile.write(profile.start_ns + 2, sample) - assert not profile.active - - -class NoopScheduler(Scheduler): - def setup(self) -> None: - pass - - def teardown(self) -> None: - pass - - def ensure_running(self) -> None: - pass - - -current_thread = threading.current_thread() -thread_metadata = { - str(current_thread.ident): { - "name": str(current_thread.name), - }, -} - - -sample_stacks = [ - extract_stack( - get_frame(), - LRUCache(max_size=1), - max_stack_depth=1, - cwd=os.getcwd(), - ), - extract_stack( - get_frame(), - LRUCache(max_size=1), - max_stack_depth=2, - cwd=os.getcwd(), - ), -] - - -@pytest.mark.parametrize( - ("samples", "expected"), - [ - pytest.param( - [], - { - "frames": [], - "samples": [], - "stacks": [], - "thread_metadata": thread_metadata, - }, - id="empty", - ), - pytest.param( - [(6, [("1", sample_stacks[0])])], - { - "frames": [], - "samples": [], - "stacks": [], - "thread_metadata": thread_metadata, - }, - id="single sample out of range", - ), - pytest.param( - [(0, [("1", sample_stacks[0])])], - { - "frames": [sample_stacks[0][2][0]], - "samples": [ - { - "elapsed_since_start_ns": "0", - "thread_id": "1", - "stack_id": 0, - }, - ], - "stacks": [[0]], - "thread_metadata": thread_metadata, - }, - id="single sample in range", - ), - pytest.param( - [ - (0, [("1", sample_stacks[0])]), - (1, [("1", sample_stacks[0])]), - ], - { - "frames": [sample_stacks[0][2][0]], - "samples": [ - { - "elapsed_since_start_ns": "0", - "thread_id": "1", - "stack_id": 0, - }, - { - "elapsed_since_start_ns": "1", - "thread_id": "1", - "stack_id": 0, - }, - ], - "stacks": [[0]], - "thread_metadata": thread_metadata, - }, - id="two identical stacks", - ), - pytest.param( - [ - (0, [("1", sample_stacks[0])]), - (1, [("1", sample_stacks[1])]), - ], - { - "frames": [ - sample_stacks[0][2][0], - sample_stacks[1][2][0], - ], - "samples": [ - { - "elapsed_since_start_ns": "0", - "thread_id": "1", - "stack_id": 0, - }, - { - "elapsed_since_start_ns": "1", - "thread_id": "1", - "stack_id": 1, - }, - ], - "stacks": [[0], [1, 0]], - "thread_metadata": thread_metadata, - }, - id="two different stacks", - ), - ], -) -@mock.patch("sentry_sdk.profiler.transaction_profiler.MAX_PROFILE_DURATION_NS", 5) -def test_profile_processing( - DictionaryContaining, # noqa: N803 - samples, - expected, -): - with NoopScheduler(frequency=1000) as scheduler: - with Profile(True, 0, scheduler=scheduler) as profile: - for ts, sample in samples: - # force the sample to be written at a time relative to the - # start of the profile - now = profile.start_ns + ts - profile.write(now, sample) - - processed = profile.process() - - assert processed["thread_metadata"] == DictionaryContaining( - expected["thread_metadata"] - ) - assert processed["frames"] == expected["frames"] - assert processed["stacks"] == expected["stacks"] - assert processed["samples"] == expected["samples"] - - -def test_hub_backwards_compatibility(suppress_deprecation_warnings): - hub = sentry_sdk.Hub() - - with pytest.warns(DeprecationWarning): - profile = Profile(True, 0, hub=hub) - - with pytest.warns(DeprecationWarning): - assert profile.hub is hub - - new_hub = sentry_sdk.Hub() - - with pytest.warns(DeprecationWarning): - profile.hub = new_hub - - with pytest.warns(DeprecationWarning): - assert profile.hub is new_hub - - -def test_no_warning_without_hub(): - with warnings.catch_warnings(): - warnings.simplefilter("error") - Profile(True, 0) diff --git a/tests/test_basics.py b/tests/test_basics.py index 01d518eee6..989b5d7941 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -11,7 +11,6 @@ import sentry_sdk import sentry_sdk.scope from sentry_sdk import ( - Hub, add_breadcrumb, capture_event, capture_exception, @@ -23,7 +22,6 @@ push_scope, start_transaction, ) -from sentry_sdk.client import Client from sentry_sdk.integrations import ( _AUTO_ENABLING_INTEGRATIONS, _DEFAULT_INTEGRATIONS, @@ -313,59 +311,6 @@ def test_push_scope(sentry_init, capture_events, suppress_deprecation_warnings): assert "exception" in event -def test_push_scope_null_client( - sentry_init, capture_events, suppress_deprecation_warnings -): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - sentry_init() - events = capture_events() - - Hub.current.bind_client(None) - - with push_scope() as scope: - scope.level = "warning" - try: - 1 / 0 - except Exception as e: - capture_exception(e) - - assert len(events) == 0 - - -@pytest.mark.skip( - reason="This test is not valid anymore, because push_scope just returns the isolation scope. This test should be removed once the Hub is removed" -) -@pytest.mark.parametrize("null_client", (True, False)) -def test_push_scope_callback(sentry_init, null_client, capture_events): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - sentry_init() - - if null_client: - Hub.current.bind_client(None) - - outer_scope = Hub.current.scope - - calls = [] - - @push_scope - def _(scope): - assert scope is Hub.current.scope - assert scope is not outer_scope - calls.append(1) - - # push_scope always needs to execute the callback regardless of - # client state, because that actually runs usercode in it, not - # just scope config code - assert calls == [1] - - # Assert scope gets popped correctly - assert Hub.current.scope is outer_scope - - def test_breadcrumbs(sentry_init, capture_events): sentry_init(max_breadcrumbs=10) events = capture_events() @@ -636,71 +581,6 @@ def test_integrations( } == expected_integrations -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes calling bind_client on the Hub sets the client on the global scope. This test should be removed once the Hub is removed" -) -def test_client_initialized_within_scope(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.WARNING) - - sentry_init() - - with push_scope(): - Hub.current.bind_client(Client()) - - (record,) = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.msg.startswith("init() called inside of pushed scope.") - - -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes the push_scope just returns the isolation scope. This test should be removed once the Hub is removed" -) -def test_scope_leaks_cleaned_up(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.WARNING) - - sentry_init() - - old_stack = list(Hub.current._stack) - - with push_scope(): - push_scope() - - assert Hub.current._stack == old_stack - - (record,) = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.message.startswith("Leaked 1 scopes:") - - -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes there is not pushing and popping of scopes. This test should be removed once the Hub is removed" -) -def test_scope_popped_too_soon(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.ERROR) - - sentry_init() - - old_stack = list(Hub.current._stack) - - with push_scope(): - Hub.current.pop_scope_unsafe() - - assert Hub.current._stack == old_stack - - (record,) = (x for x in caplog.records if x.levelname == "ERROR") - - assert record.message == ("Scope popped too soon. Popped 1 scopes too many.") - - def test_scope_event_processor_order(sentry_init, capture_events): def before_send(event, hint): event["message"] += "baz" @@ -1127,24 +1007,6 @@ def test_last_event_id_scope(sentry_init): assert scope.last_event_id() is None -def test_hub_constructor_deprecation_warning(): - with pytest.warns(sentry_sdk.hub.SentryHubDeprecationWarning): - Hub() - - -def test_hub_current_deprecation_warning(): - with pytest.warns(sentry_sdk.hub.SentryHubDeprecationWarning) as warning_records: - Hub.current - - # Make sure we only issue one deprecation warning - assert len(warning_records) == 1 - - -def test_hub_main_deprecation_warnings(): - with pytest.warns(sentry_sdk.hub.SentryHubDeprecationWarning): - Hub.main - - @pytest.mark.skipif(sys.version_info < (3, 11), reason="add_note() not supported") def test_notes(sentry_init, capture_events): sentry_init() diff --git a/tests/test_client.py b/tests/test_client.py index 78868e434a..8a71dcc778 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -15,7 +15,6 @@ import sentry_sdk from sentry_sdk import ( Client, - Hub, add_breadcrumb, capture_event, capture_exception, @@ -635,31 +634,6 @@ def capture_envelope(self, envelope): assert output.count(b"HI") == num_messages -def test_configure_scope_available( - sentry_init, request, monkeypatch, suppress_deprecation_warnings -): - """ - Test that scope is configured if client is configured - - This test can be removed once configure_scope and the Hub are removed. - """ - sentry_init() - - with configure_scope() as scope: - assert scope is Hub.current.scope - scope.set_tag("foo", "bar") - - calls = [] - - def callback(scope): - calls.append(scope) - scope.set_tag("foo", "bar") - - assert configure_scope(callback) is None - assert len(calls) == 1 - assert calls[0] is Hub.current.scope - - @pytest.mark.tests_internal_exceptions def test_client_debug_option_enabled(sentry_init, caplog): sentry_init(debug=True) @@ -1351,7 +1325,7 @@ def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags): uwsgi = mock.MagicMock() uwsgi.opt = opt with mock.patch.dict("sys.modules", uwsgi=uwsgi): - sentry_init(profiles_sample_rate=1.0) + sentry_init() if missing_flags: assert len(recwarn) == 1 record = recwarn.pop() diff --git a/tests/test_envelope.py b/tests/test_envelope.py index 5549cc6e36..f6a7c4b541 100644 --- a/tests/test_envelope.py +++ b/tests/test_envelope.py @@ -250,7 +250,6 @@ def test_envelope_item_data_category_mapping(): ("session", "session"), ("attachment", "attachment"), ("client_report", "internal"), - ("profile", "profile"), ("profile_chunk", "profile_chunk"), ("check_in", "monitor"), ("unknown_type", "default"), diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 731b188727..7f4fdf6748 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -1,7 +1,7 @@ from unittest import mock import sentry_sdk -from sentry_sdk.sessions import auto_session_tracking, track_session +from sentry_sdk.sessions import track_session def sorted_aggregates(item): @@ -83,47 +83,6 @@ def test_aggregates(sentry_init, capture_envelopes): assert aggregates[0]["errored"] == 1 -def test_aggregates_deprecated( - sentry_init, capture_envelopes, suppress_deprecation_warnings -): - sentry_init( - release="fun-release", - environment="not-fun-env", - ) - envelopes = capture_envelopes() - - with auto_session_tracking(session_mode="request"): - with sentry_sdk.new_scope() as scope: - try: - scope.set_user({"id": "42"}) - raise Exception("all is wrong") - except Exception: - sentry_sdk.capture_exception() - - with auto_session_tracking(session_mode="request"): - pass - - sentry_sdk.get_isolation_scope().start_session(session_mode="request") - sentry_sdk.get_isolation_scope().end_session() - sentry_sdk.flush() - - assert len(envelopes) == 2 - assert envelopes[0].get_event() is not None - - sess = envelopes[1] - assert len(sess.items) == 1 - sess_event = sess.items[0].payload.json - assert sess_event["attrs"] == { - "release": "fun-release", - "environment": "not-fun-env", - } - - aggregates = sorted_aggregates(sess_event) - assert len(aggregates) == 1 - assert aggregates[0]["exited"] == 2 - assert aggregates[0]["errored"] == 1 - - def test_aggregates_explicitly_disabled_session_tracking_request_mode( sentry_init, capture_envelopes ): @@ -157,38 +116,6 @@ def test_aggregates_explicitly_disabled_session_tracking_request_mode( assert "errored" not in aggregates[0] -def test_aggregates_explicitly_disabled_session_tracking_request_mode_deprecated( - sentry_init, capture_envelopes, suppress_deprecation_warnings -): - sentry_init( - release="fun-release", environment="not-fun-env", auto_session_tracking=False - ) - envelopes = capture_envelopes() - - with auto_session_tracking(session_mode="request"): - with sentry_sdk.new_scope(): - try: - raise Exception("all is wrong") - except Exception: - sentry_sdk.capture_exception() - - with auto_session_tracking(session_mode="request"): - pass - - sentry_sdk.get_isolation_scope().start_session(session_mode="request") - sentry_sdk.get_isolation_scope().end_session() - sentry_sdk.flush() - - sess = envelopes[1] - assert len(sess.items) == 1 - sess_event = sess.items[0].payload.json - - aggregates = sorted_aggregates(sess_event) - assert len(aggregates) == 1 - assert aggregates[0]["exited"] == 1 - assert "errored" not in aggregates[0] - - def test_no_thread_on_shutdown_no_errors(sentry_init): sentry_init( release="fun-release", @@ -218,36 +145,6 @@ def test_no_thread_on_shutdown_no_errors(sentry_init): # If we reach this point without error, the test is successful. -def test_no_thread_on_shutdown_no_errors_deprecated( - sentry_init, suppress_deprecation_warnings -): - sentry_init( - release="fun-release", - environment="not-fun-env", - ) - - # make it seem like the interpreter is shutting down - with mock.patch( - "threading.Thread.start", - side_effect=RuntimeError("can't create new thread at interpreter shutdown"), - ): - with auto_session_tracking(session_mode="request"): - with sentry_sdk.new_scope(): - try: - raise Exception("all is wrong") - except Exception: - sentry_sdk.capture_exception() - - with auto_session_tracking(session_mode="request"): - pass - - sentry_sdk.get_isolation_scope().start_session(session_mode="request") - sentry_sdk.get_isolation_scope().end_session() - sentry_sdk.flush() - - # If we reach this point without error, the test is successful. - - def test_top_level_start_session_basic(sentry_init, capture_envelopes): """Test that top-level start_session starts a session on the isolation scope.""" sentry_init(release="test-release", environment="test-env") diff --git a/tests/test_transport.py b/tests/test_transport.py index 8f74b66eed..044594f457 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -29,7 +29,6 @@ import sentry_sdk from sentry_sdk import ( Client, - Hub, add_breadcrumb, capture_message, get_isolation_scope, @@ -42,7 +41,6 @@ from sentry_sdk.transport import ( KEEP_ALIVE_SOCKET_OPTIONS, AsyncHttpTransport, - HttpTransport, _parse_rate_limits, ) @@ -802,24 +800,6 @@ def test_log_item_limits(capturing_server, response_code, item, make_client): } in report["discarded_events"] -def test_hub_cls_backwards_compat(): - class TestCustomHubClass(Hub): - pass - - transport = HttpTransport( - defaultdict(lambda: None, {"dsn": "https://123abc@example.com/123"}) - ) - - with pytest.deprecated_call(): - assert transport.hub_cls is Hub - - with pytest.deprecated_call(): - transport.hub_cls = TestCustomHubClass - - with pytest.deprecated_call(): - assert transport.hub_cls is TestCustomHubClass - - @pytest.mark.parametrize("quantity", (1, 2, 10)) def test_record_lost_event_quantity(capturing_server, make_client, quantity): client = make_client() diff --git a/tests/tracing/test_deprecated.py b/tests/tracing/test_deprecated.py index f030a465ff..8b8150f5d1 100644 --- a/tests/tracing/test_deprecated.py +++ b/tests/tracing/test_deprecated.py @@ -1,36 +1,9 @@ import warnings -import pytest - import sentry_sdk import sentry_sdk.tracing -@pytest.mark.parametrize( - "parameter_value_getter", - # Use lambda to avoid Hub deprecation warning here (will suppress it in the test) - (lambda: sentry_sdk.Hub(), lambda: sentry_sdk.Scope()), -) -def test_passing_hub_parameter_to_transaction_finish( - suppress_deprecation_warnings, parameter_value_getter -): - parameter_value = parameter_value_getter() - transaction = sentry_sdk.tracing.Transaction() - with pytest.warns(DeprecationWarning): - transaction.finish(hub=parameter_value) - - -def test_passing_hub_object_to_scope_transaction_finish(suppress_deprecation_warnings): - transaction = sentry_sdk.tracing.Transaction() - - # Do not move the following line under the `with` statement. Otherwise, the Hub.__init__ deprecation - # warning will be confused with the transaction.finish deprecation warning that we are testing. - hub = sentry_sdk.Hub() - - with pytest.warns(DeprecationWarning): - transaction.finish(hub) - - def test_no_warnings_scope_to_transaction_finish(): transaction = sentry_sdk.tracing.Transaction() with warnings.catch_warnings(): diff --git a/tests/tracing/test_noop_span.py b/tests/tracing/test_noop_span.py deleted file mode 100644 index 36778cd485..0000000000 --- a/tests/tracing/test_noop_span.py +++ /dev/null @@ -1,52 +0,0 @@ -import sentry_sdk -from sentry_sdk.tracing import NoOpSpan - -# These tests make sure that the examples from the documentation [1] -# are working when OTel (OpenTelemetry) instrumentation is turned on, -# and therefore, the Sentry tracing should not do anything. -# -# 1: https://docs.sentry.io/platforms/python/performance/instrumentation/custom-instrumentation/ - - -def test_noop_start_transaction(sentry_init): - sentry_init(instrumenter="otel") - - with sentry_sdk.start_transaction( - op="task", name="test_transaction_name" - ) as transaction: - assert isinstance(transaction, NoOpSpan) - assert sentry_sdk.get_current_scope().span is transaction - - transaction.name = "new name" - - -def test_noop_start_span(sentry_init): - sentry_init(instrumenter="otel") - - with sentry_sdk.start_span(op="http", name="GET /") as span: - assert isinstance(span, NoOpSpan) - assert sentry_sdk.get_current_scope().span is span - - span.set_tag("http.response.status_code", 418) - span.set_data("http.entity_type", "teapot") - - -def test_noop_transaction_start_child(sentry_init): - sentry_init(instrumenter="otel") - - transaction = sentry_sdk.start_transaction(name="task") - assert isinstance(transaction, NoOpSpan) - - with transaction.start_child(op="child_task") as child: - assert isinstance(child, NoOpSpan) - assert sentry_sdk.get_current_scope().span is child - - -def test_noop_span_start_child(sentry_init): - sentry_init(instrumenter="otel") - span = sentry_sdk.start_span(name="task") - assert isinstance(span, NoOpSpan) - - with span.start_child(op="child_task") as child: - assert isinstance(child, NoOpSpan) - assert sentry_sdk.get_current_scope().span is child diff --git a/tox.ini b/tox.ini index 889af6d6bf..7819947094 100644 --- a/tox.ini +++ b/tox.ini @@ -42,15 +42,9 @@ envlist = # GCP {py3.7}-gcp - # OpenTelemetry (OTel) - {py3.7,py3.9,py3.12,py3.13,py3.14,py3.14t}-opentelemetry - # OpenTelemetry with OTLP {py3.7,py3.9,py3.12,py3.13,py3.14}-otlp - # OpenTelemetry Experimental (POTel) - {py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-potel - # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -459,17 +453,10 @@ deps = aws_lambda: uvicorn aws_lambda: pyyaml - # OpenTelemetry (OTel) - opentelemetry: opentelemetry-distro - opentelemetry: pytest-forked - # OpenTelemetry with OTLP otlp: opentelemetry-distro[otlp] otlp: responses - - # OpenTelemetry Experimental (POTel) - potel: opentelemetry-distro - potel: pytest-forked + otlp: pytest-forked # === Integrations - Auto-generated === # These come from the populate_tox.py script. @@ -18625,9 +18612,7 @@ setenv = aws_lambda: _TESTPATH=tests/integrations/aws_lambda cloud_resource_context: _TESTPATH=tests/integrations/cloud_resource_context gcp: _TESTPATH=tests/integrations/gcp - opentelemetry: _TESTPATH=tests/integrations/opentelemetry otlp: _TESTPATH=tests/integrations/otlp - potel: _TESTPATH=tests/integrations/opentelemetry socket: _TESTPATH=tests/integrations/socket # These TESTPATH definitions are auto-generated by toxgen