From 9d97d93a7a3ccfef3f4796b5429716188e4aaec1 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:58:27 +0200 Subject: [PATCH 01/19] ref: Stop using `Hub` in `tracing_utils` (#3269) Get the client via `sentry_sdk.get_client()` instead. Prerequisite for #3265 --- sentry_sdk/tracing_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index a3a03e65c1..ba20dc8436 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -492,9 +492,9 @@ def from_options(cls, scope): third_party_items = "" mutable = False - client = sentry_sdk.Hub.current.client + client = sentry_sdk.get_client() - if client is None or scope._propagation_context is None: + if not client.is_active() or scope._propagation_context is None: return Baggage(sentry_items) options = client.options From 1f17f46472511a22365f8da020b9c0b3933d1286 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 12:18:28 +0200 Subject: [PATCH 02/19] ref(types): Correct `ExcInfo` type Previously, we defined `ExcInfo` as `tuple[Type[BaseException] | None, BaseException | None, TracebackType | None]`, when in fact, the correct type is the narrower `tuple[Type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None]`. --- sentry_sdk/_types.py | 5 +++-- sentry_sdk/integrations/sanic.py | 5 ++--- sentry_sdk/utils.py | 9 ++++++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 14fa8d08c2..b82376e517 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -121,8 +121,9 @@ total=False, ) - ExcInfo = Tuple[ - Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType] + ExcInfo = Union[ + tuple[Type[BaseException], BaseException, Optional[TracebackType]], + tuple[None, None, None], ] Hint = Dict[str, Any] diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index f2f9b8168e..46250926ef 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -28,13 +28,12 @@ from typing import Callable from typing import Optional from typing import Union - from typing import Tuple from typing import Dict from sanic.request import Request, RequestParameters from sanic.response import BaseHTTPResponse - from sentry_sdk._types import Event, EventProcessor, Hint + from sentry_sdk._types import Event, EventProcessor, ExcInfo, Hint from sanic.router import Route try: @@ -325,7 +324,7 @@ def _legacy_router_get(self, *args): @ensure_integration_enabled(SanicIntegration) def _capture_exception(exception): - # type: (Union[Tuple[Optional[type], Optional[BaseException], Any], BaseException]) -> None + # type: (Union[ExcInfo, BaseException]) -> None with capture_internal_exceptions(): event, hint = event_from_exception( exception, diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index a84f2eb3de..935172333f 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1019,7 +1019,14 @@ def exc_info_from_error(error): else: raise ValueError("Expected Exception object to report, got %s!" % type(error)) - return exc_type, exc_value, tb + exc_info = (exc_type, exc_value, tb) + + if TYPE_CHECKING: + # This cast is safe because exc_type and exc_value are either both + # None or both not None. + exc_info = cast(ExcInfo, exc_info) + + return exc_info def event_from_exception( From 2a0e8831633904531f2fd3f26f4d9cbb1d2eba8b Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 12:45:30 +0200 Subject: [PATCH 03/19] ref(scope): Improve `Scope._capture_internal_exception` type hint --- sentry_sdk/scope.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index ee46452d21..5a271eff44 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1191,9 +1191,9 @@ def capture_exception(self, error=None, scope=None, **scope_kwargs): return None def _capture_internal_exception( - self, exc_info # type: Any + self, exc_info # type: ExcInfo ): - # type: (...) -> Any + # type: (...) -> None """ Capture an exception that is likely caused by a bug in the SDK itself. From f3c8f9f9ed5386bc89d60f781b33011635a5c206 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 12:03:41 +0200 Subject: [PATCH 04/19] ref: Remove Hub from `capture_internal_exception` logic --- sentry_sdk/debug.py | 14 ++++++-------- sentry_sdk/hub.py | 18 ------------------ sentry_sdk/scope.py | 7 +++---- sentry_sdk/utils.py | 11 ++--------- tests/conftest.py | 5 +++-- 5 files changed, 14 insertions(+), 41 deletions(-) diff --git a/sentry_sdk/debug.py b/sentry_sdk/debug.py index c99f85558d..9291813cae 100644 --- a/sentry_sdk/debug.py +++ b/sentry_sdk/debug.py @@ -1,9 +1,8 @@ import sys import logging +import warnings -from sentry_sdk import utils from sentry_sdk.client import _client_init_debug -from sentry_sdk.hub import Hub from sentry_sdk.scope import Scope from sentry_sdk.utils import logger from logging import LogRecord @@ -22,7 +21,6 @@ def init_debug_support(): # type: () -> None if not logger.handlers: configure_logger() - configure_debug_hub() def configure_logger(): @@ -36,8 +34,8 @@ def configure_logger(): def configure_debug_hub(): # type: () -> None - def _get_debug_hub(): - # type: () -> Hub - return Hub.current - - utils._get_debug_hub = _get_debug_hub + warnings.warn( + "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index f5a87113c2..3dfb79620a 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -414,24 +414,6 @@ def capture_exception(self, error=None, scope=None, **scope_kwargs): return last_event_id - def _capture_internal_exception( - self, exc_info # type: Any - ): - # type: (...) -> 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._capture_internal_exception` instead. - - Capture an exception that is likely caused by a bug in the SDK - itself. - - Duplicated in :py:meth:`sentry_sdk.client._Client._capture_internal_exception`. - - These exceptions do not end up in Sentry and are just logged instead. - """ - logger.error("Internal error in sentry_sdk", exc_info=exc_info) - def add_breadcrumb(self, crumb=None, hint=None, **kwargs): # type: (Optional[Breadcrumb], Optional[BreadcrumbHint], Any) -> None """ diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 5a271eff44..b4274a4e7c 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1190,10 +1190,9 @@ def capture_exception(self, error=None, scope=None, **scope_kwargs): return None - def _capture_internal_exception( - self, exc_info # type: ExcInfo - ): - # type: (...) -> None + @staticmethod + def _capture_internal_exception(exc_info): + # type: (ExcInfo) -> None """ Capture an exception that is likely caused by a bug in the SDK itself. diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 935172333f..2079be52cc 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -81,12 +81,6 @@ def json_dumps(data): return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8") -def _get_debug_hub(): - # type: () -> Optional[sentry_sdk.Hub] - # This function is replaced by debug.py - pass - - def get_git_revision(): # type: () -> Optional[str] try: @@ -198,9 +192,8 @@ def capture_internal_exceptions(): def capture_internal_exception(exc_info): # type: (ExcInfo) -> None - hub = _get_debug_hub() - if hub is not None: - hub._capture_internal_exception(exc_info) + if sentry_sdk.get_client().is_active(): + sentry_sdk.Scope._capture_internal_exception(exc_info) def to_timestamp(value): diff --git a/tests/conftest.py b/tests/conftest.py index eada3bdac7..8a4af3e98c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -78,7 +78,8 @@ def internal_exceptions(request, monkeypatch): if "tests_internal_exceptions" in request.keywords: return - def _capture_internal_exception(self, exc_info): + @staticmethod + def _capture_internal_exception(exc_info): errors.append(exc_info) @request.addfinalizer @@ -89,7 +90,7 @@ def _(): reraise(*e) monkeypatch.setattr( - sentry_sdk.Hub, "_capture_internal_exception", _capture_internal_exception + sentry_sdk.Scope, "_capture_internal_exception", _capture_internal_exception ) return errors From 3461068b00c8ac40d65c4568e514586568282122 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 14:15:23 +0200 Subject: [PATCH 05/19] ref(tracing): Remove `Hub` in `Transaction.finish` Rename `Transaction.finish` method's `hub` parameter to `scope` (in a backwards-compatible manner), and update the method so that it is using `Scope` API under the hood as much as possible. Prerequisite for #3265 --- sentry_sdk/tracing.py | 75 ++++++++++++++++++++++++++++---- tests/tracing/test_deprecated.py | 25 +++++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 95a2d3469b..80a38b1e43 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,5 +1,6 @@ import uuid import random +import warnings from datetime import datetime, timedelta, timezone import sentry_sdk @@ -286,13 +287,23 @@ def __init__( self.op = op self.description = description self.status = status - self.hub = hub + self.hub = hub # backwards compatibility self.scope = scope self.origin = origin self._measurements = {} # type: Dict[str, MeasurementValue] self._tags = {} # type: MutableMapping[str, str] self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction + + if hub is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use `scope` instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.scope = self.scope or hub.scope + if start_timestamp is None: start_timestamp = datetime.now(timezone.utc) elif isinstance(start_timestamp, float): @@ -823,15 +834,57 @@ def containing_transaction(self): # reference. return self - def finish(self, hub=None, end_timestamp=None): - # type: (Optional[Union[sentry_sdk.Hub, sentry_sdk.Scope]], Optional[Union[float, datetime]]) -> Optional[str] + def _get_scope_from_finish_args( + self, + scope_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] + hub_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] + ): + # type: (...) -> Optional[sentry_sdk.Scope] + """ + Logic to get the scope from the arguments passed to finish. This + function exists for backwards compatibility with the old finish. + + TODO: Remove this function in the next major version. + """ + scope_or_hub = scope_arg + if hub_arg is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", + DeprecationWarning, + stacklevel=3, + ) + + scope_or_hub = hub_arg + + if isinstance(scope_or_hub, sentry_sdk.Hub): + warnings.warn( + "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", + DeprecationWarning, + stacklevel=3, + ) + + return scope_or_hub.scope + + return scope_or_hub + + def finish( + self, + scope=None, # type: Optional[sentry_sdk.Scope] + end_timestamp=None, # type: Optional[Union[float, datetime]] + *, + hub=None, # type: Optional[sentry_sdk.Hub] + ): + # type: (...) -> Optional[str] """Finishes the transaction and sends it to Sentry. All finished spans in the transaction will also be sent to Sentry. - :param hub: The hub to use for this transaction. - If not provided, the current hub will be used. + :param scope: The Scope to use for this transaction. + If not provided, the current Scope will be used. :param end_timestamp: Optional timestamp that should be used as timestamp instead of the current time. + :param hub: The hub to use for this transaction. + This argument is DEPRECATED. Please use the `scope` + parameter, instead. :return: The event ID if the transaction was sent to Sentry, otherwise None. @@ -840,7 +893,13 @@ def finish(self, hub=None, end_timestamp=None): # This transaction is already finished, ignore. return None - hub = hub or self.hub or sentry_sdk.Hub.current + # For backwards compatibility, we must handle the case where `scope` + # or `hub` could both either be a `Scope` or a `Hub`. + scope = self._get_scope_from_finish_args( + scope, hub + ) # type: Optional[sentry_sdk.Scope] + + scope = scope or self.scope or sentry_sdk.Scope.get_current_scope() client = sentry_sdk.Scope.get_client() if not client.is_active(): @@ -877,7 +936,7 @@ def finish(self, hub=None, end_timestamp=None): ) self.name = "" - super().finish(hub, end_timestamp) + super().finish(scope, end_timestamp) if not self.sampled: # At this point a `sampled = None` should have already been resolved @@ -930,7 +989,7 @@ def finish(self, hub=None, end_timestamp=None): if metrics_summary: event["_metrics_summary"] = metrics_summary - return hub.capture_event(event) + return scope.capture_event(event) def set_measurement(self, name, value, unit=""): # type: (str, float, MeasurementUnit) -> None diff --git a/tests/tracing/test_deprecated.py b/tests/tracing/test_deprecated.py index ba296350ec..8b7f34b6cb 100644 --- a/tests/tracing/test_deprecated.py +++ b/tests/tracing/test_deprecated.py @@ -1,4 +1,9 @@ +import warnings + import pytest + +import sentry_sdk +import sentry_sdk.tracing from sentry_sdk import start_span from sentry_sdk.tracing import Span @@ -20,3 +25,23 @@ def test_start_span_to_start_transaction(sentry_init, capture_events): assert len(events) == 2 assert events[0]["transaction"] == "/1/" assert events[1]["transaction"] == "/2/" + + +@pytest.mark.parametrize("parameter_value", (sentry_sdk.Hub(), sentry_sdk.Scope())) +def test_passing_hub_parameter_to_transaction_finish(parameter_value): + transaction = sentry_sdk.tracing.Transaction() + with pytest.warns(DeprecationWarning): + transaction.finish(hub=parameter_value) + + +def test_passing_hub_object_to_scope_transaction_finish(): + transaction = sentry_sdk.tracing.Transaction() + with pytest.warns(DeprecationWarning): + transaction.finish(sentry_sdk.Hub()) + + +def test_no_warnings_scope_to_transaction_finish(): + transaction = sentry_sdk.tracing.Transaction() + with warnings.catch_warnings(): + warnings.simplefilter("error") + transaction.finish(sentry_sdk.Scope()) From 1c86489192c9ae8c2a830870c68bd8f998bb960a Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 16:33:05 +0200 Subject: [PATCH 06/19] ref(tracing): Update `NoOpSpan.finish` signature Make the same changes previously made to `Transaction.finish`. --- sentry_sdk/tracing.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 80a38b1e43..f1f3200035 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1205,8 +1205,17 @@ def get_profile_context(self): # type: () -> Any return {} - def finish(self, hub=None, end_timestamp=None): - # type: (Optional[Union[sentry_sdk.Hub, sentry_sdk.Scope]], Optional[Union[float, datetime]]) -> Optional[str] + def finish( + self, + scope=None, # type: Optional[sentry_sdk.Scope] + end_timestamp=None, # type: Optional[Union[float, datetime]] + *, + hub=None, # type: Optional[sentry_sdk.Hub] + ): + # type: (...) -> Optional[str] + """ + The `hub` parameter is deprecated. Please use the `scope` parameter, instead. + """ pass def set_measurement(self, name, value, unit=""): From c359c82ea743f8e2d2e7f46ba09c83af619bc615 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:03:20 +0200 Subject: [PATCH 07/19] ref(debug): Rename debug logging filter (#3260) Previous name said that this filter was "hub-based," when the logic in reality is not related to hubs. So, we should rename the filter to something more sensible. --- sentry_sdk/debug.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/debug.py b/sentry_sdk/debug.py index 9291813cae..e30b471698 100644 --- a/sentry_sdk/debug.py +++ b/sentry_sdk/debug.py @@ -8,7 +8,7 @@ from logging import LogRecord -class _HubBasedClientFilter(logging.Filter): +class _DebugFilter(logging.Filter): def filter(self, record): # type: (LogRecord) -> bool if _client_init_debug.get(False): @@ -29,7 +29,7 @@ def configure_logger(): _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) logger.addHandler(_handler) logger.setLevel(logging.DEBUG) - logger.addFilter(_HubBasedClientFilter()) + logger.addFilter(_DebugFilter()) def configure_debug_hub(): From cfcd5b1f30e40b3bbf7c1228545f6df23748ede0 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 17:49:12 +0200 Subject: [PATCH 08/19] test: Remove `Hub` usage in `conftest` --- tests/conftest.py | 15 +++++++++------ tests/new_scopes_compat/__init__.py | 7 +++++++ tests/new_scopes_compat/conftest.py | 8 ++++++++ .../test_new_scopes_compat.py | 0 .../test_new_scopes_compat_event.py | 4 ++-- 5 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 tests/new_scopes_compat/__init__.py create mode 100644 tests/new_scopes_compat/conftest.py rename tests/{ => new_scopes_compat}/test_new_scopes_compat.py (100%) rename tests/{ => new_scopes_compat}/test_new_scopes_compat_event.py (98%) diff --git a/tests/conftest.py b/tests/conftest.py index 8a4af3e98c..048f8bc140 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -186,10 +186,9 @@ def reset_integrations(): @pytest.fixture def sentry_init(request): def inner(*a, **kw): - hub = sentry_sdk.Hub.current kw.setdefault("transport", TestTransport()) client = sentry_sdk.Client(*a, **kw) - hub.bind_client(client) + sentry_sdk.Scope.get_global_scope().set_client(client) if request.node.get_closest_marker("forked"): # Do not run isolation if the test is already running in @@ -197,8 +196,12 @@ def inner(*a, **kw): # fork) yield inner else: - with sentry_sdk.Hub(None): + old_client = sentry_sdk.Scope.get_global_scope().client + try: + sentry_sdk.Scope.get_current_scope().set_client(None) yield inner + finally: + sentry_sdk.Scope.get_global_scope().set_client(old_client) class TestTransport(Transport): @@ -214,7 +217,7 @@ def capture_envelope(self, _: Envelope) -> None: def capture_events(monkeypatch): def inner(): events = [] - test_client = sentry_sdk.Hub.current.client + test_client = sentry_sdk.get_client() old_capture_envelope = test_client.transport.capture_envelope def append_event(envelope): @@ -234,7 +237,7 @@ def append_event(envelope): def capture_envelopes(monkeypatch): def inner(): envelopes = [] - test_client = sentry_sdk.Hub.current.client + test_client = sentry_sdk.get_client() old_capture_envelope = test_client.transport.capture_envelope def append_envelope(envelope): @@ -274,7 +277,7 @@ def inner(): events_r = os.fdopen(events_r, "rb", 0) events_w = os.fdopen(events_w, "wb", 0) - test_client = sentry_sdk.Hub.current.client + test_client = sentry_sdk.get_client() old_capture_envelope = test_client.transport.capture_envelope diff --git a/tests/new_scopes_compat/__init__.py b/tests/new_scopes_compat/__init__.py new file mode 100644 index 0000000000..45391bd9ad --- /dev/null +++ b/tests/new_scopes_compat/__init__.py @@ -0,0 +1,7 @@ +""" +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 new file mode 100644 index 0000000000..3afcf91704 --- /dev/null +++ b/tests/new_scopes_compat/conftest.py @@ -0,0 +1,8 @@ +import pytest +import sentry_sdk + + +@pytest.fixture(autouse=True) +def isolate_hub(): + with sentry_sdk.Hub(None): + yield diff --git a/tests/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py similarity index 100% rename from tests/test_new_scopes_compat.py rename to tests/new_scopes_compat/test_new_scopes_compat.py diff --git a/tests/test_new_scopes_compat_event.py b/tests/new_scopes_compat/test_new_scopes_compat_event.py similarity index 98% rename from tests/test_new_scopes_compat_event.py rename to tests/new_scopes_compat/test_new_scopes_compat_event.py index 53eb095b5e..fd43a25c69 100644 --- a/tests/test_new_scopes_compat_event.py +++ b/tests/new_scopes_compat/test_new_scopes_compat_event.py @@ -32,10 +32,10 @@ def create_expected_error_event(trx, span): "stacktrace": { "frames": [ { - "filename": "tests/test_new_scopes_compat_event.py", + "filename": "tests/new_scopes_compat/test_new_scopes_compat_event.py", "abs_path": mock.ANY, "function": "_faulty_function", - "module": "tests.test_new_scopes_compat_event", + "module": "tests.new_scopes_compat.test_new_scopes_compat_event", "lineno": mock.ANY, "pre_context": [ " return create_expected_transaction_event", From 7996dca843dd77643369af6aa88f5304890c4957 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 17:53:28 +0200 Subject: [PATCH 09/19] ref(hub): Delete `_should_send_default_pii` We don't use this function, and since it is marked as a private method, that means we can delete it. --- sentry_sdk/hub.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 3dfb79620a..b9b933e27b 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -59,16 +59,6 @@ def overload(x): _local = ContextVar("sentry_current_hub") -def _should_send_default_pii(): - # type: () -> bool - # TODO: Migrate existing code to `scope.should_send_default_pii()` and remove this function. - # New code should not use this function! - client = Hub.current.client - if not client: - return False - return client.should_send_default_pii() - - class _InitGuard: def __init__(self, client): # type: (Client) -> None From 1e82809d89a7bbe63365f96167d2dee1bdff6ca1 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 10 Jul 2024 18:26:51 +0200 Subject: [PATCH 10/19] ref(init): Stop using `Hub` in `init` Use `Scope` APIs only in implementation for `sentry_sdk.init`, rather than `Hub` APIs. --- sentry_sdk/hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index b9b933e27b..8e114a7de4 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -90,7 +90,7 @@ def _init(*args, **kwargs): This takes the same arguments as the client constructor. """ client = Client(*args, **kwargs) # type: ignore - Hub.current.bind_client(client) + Scope.get_global_scope().set_client(client) _check_python_deprecations() rv = _InitGuard(client) return rv From 06d5da1180ad7d5a3593593d2fba98408a3b40b7 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:30:04 +0200 Subject: [PATCH 11/19] ref(profiling): Deprecate `hub` in `Profile` (#3270) Related to #3265 --- sentry_sdk/profiler/transaction_profiler.py | 32 ++++++++++++++++++++- tests/profiler/test_transaction_profiler.py | 26 +++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/profiler/transaction_profiler.py b/sentry_sdk/profiler/transaction_profiler.py index bdd6c5fa8c..e8ebfa6450 100644 --- a/sentry_sdk/profiler/transaction_profiler.py +++ b/sentry_sdk/profiler/transaction_profiler.py @@ -33,6 +33,7 @@ import threading import time import uuid +import warnings from abc import ABC, abstractmethod from collections import deque @@ -213,7 +214,6 @@ def __init__( ): # type: (...) -> None self.scheduler = _scheduler if scheduler is None else scheduler - self.hub = hub self.event_id = uuid.uuid4().hex # type: str @@ -240,6 +240,16 @@ def __init__( self.unique_samples = 0 + # Backwards compatibility with the old hub property + self._hub = None # type: Optional[sentry_sdk.Hub] + 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): # type: () -> None self.active_thread_id = get_current_thread_meta()[0] @@ -506,6 +516,26 @@ def valid(self): return True + @property + def hub(self): + # type: () -> 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): + # type: (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 = "unknown" # type: ProfilerMode diff --git a/tests/profiler/test_transaction_profiler.py b/tests/profiler/test_transaction_profiler.py index ec506cfa67..d657bec506 100644 --- a/tests/profiler/test_transaction_profiler.py +++ b/tests/profiler/test_transaction_profiler.py @@ -1,8 +1,10 @@ import inspect import os +import sentry_sdk import sys import threading import time +import warnings from collections import defaultdict from unittest import mock @@ -813,3 +815,27 @@ def test_profile_processing( assert processed["frames"] == expected["frames"] assert processed["stacks"] == expected["stacks"] assert processed["samples"] == expected["samples"] + + +def test_hub_backwards_compatibility(): + 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) From 4fb51f2d03351197824d0641fb0fd26779458f1d Mon Sep 17 00:00:00 2001 From: Grammy Jiang <719388+grammy-jiang@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:38:04 +1000 Subject: [PATCH 12/19] Add the client cert and key support to HttpTransport (#3258) * Add the client cert and key support to HttpTransport * Add a test case for the two-way ssl support in HttpTransport * Move cert_file and key_file to the end of arguments in ClientConstructor in consts.py --------- Co-authored-by: Neel Shah --- sentry_sdk/consts.py | 2 ++ sentry_sdk/transport.py | 13 ++++++++++--- tests/test_transport.py | 12 ++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 54de9d97e2..23920a2aa0 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -532,6 +532,8 @@ def __init__( enable_db_query_source=True, # type: bool db_query_source_threshold_ms=100, # type: int spotlight=None, # type: Optional[Union[bool, str]] + cert_file=None, # type: Optional[str] + key_file=None, # type: Optional[str] ): # type: (...) -> None pass diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 63bd1d9fb3..e5c39c48e4 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -226,6 +226,8 @@ def __init__( http_proxy=options["http_proxy"], https_proxy=options["https_proxy"], ca_certs=options["ca_certs"], + cert_file=options["cert_file"], + key_file=options["key_file"], proxy_headers=options["proxy_headers"], ) @@ -474,8 +476,8 @@ def _send_envelope( ) return None - def _get_pool_options(self, ca_certs): - # type: (Optional[Any]) -> Dict[str, Any] + def _get_pool_options(self, ca_certs, cert_file=None, key_file=None): + # type: (Optional[Any], Optional[Any], Optional[Any]) -> Dict[str, Any] options = { "num_pools": self._num_pools, "cert_reqs": "CERT_REQUIRED", @@ -505,6 +507,9 @@ def _get_pool_options(self, ca_certs): or certifi.where() ) + options["cert_file"] = cert_file or os.environ.get("CLIENT_CERT_FILE") + options["key_file"] = key_file or os.environ.get("CLIENT_KEY_FILE") + return options def _in_no_proxy(self, parsed_dsn): @@ -524,6 +529,8 @@ def _make_pool( http_proxy, # type: Optional[str] https_proxy, # type: Optional[str] ca_certs, # type: Optional[Any] + cert_file, # type: Optional[Any] + key_file, # type: Optional[Any] proxy_headers, # type: Optional[Dict[str, str]] ): # type: (...) -> Union[PoolManager, ProxyManager] @@ -538,7 +545,7 @@ def _make_pool( if not proxy and (http_proxy != ""): proxy = http_proxy or (not no_proxy and getproxies().get("http")) - opts = self._get_pool_options(ca_certs) + opts = self._get_pool_options(ca_certs, cert_file, key_file) if proxy: if proxy_headers: diff --git a/tests/test_transport.py b/tests/test_transport.py index dc8e8073b5..5fc81d6817 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -165,6 +165,18 @@ def test_transport_num_pools(make_client, num_pools, expected_num_pools): assert options["num_pools"] == expected_num_pools +def test_two_way_ssl_authentication(make_client): + _experiments = {} + + client = make_client(_experiments=_experiments) + + options = client.transport._get_pool_options( + [], "/path/to/cert.pem", "/path/to/key.pem" + ) + assert options["cert_file"] == "/path/to/cert.pem" + assert options["key_file"] == "/path/to/key.pem" + + def test_socket_options(make_client): socket_options = [ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), From 8a959716ad30cac6a17ecfc5a8f33ebf2b8042d1 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:02:27 +0200 Subject: [PATCH 13/19] docs(init): Fix `sentry_sdk.init` type hint (#3283) The current type hint suggests that all the parameters can be passed as positional arguments, when this is not the case. Only the `dsn` can be passed as a positional argument; the rest must be passed as keyword arguments. This PR makes the type hint reflect the reality of what parameters can be passed to `sentry_sdk.init`. --- sentry_sdk/consts.py | 14 ++++++++++++-- sentry_sdk/hub.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 23920a2aa0..f03b263162 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1,3 +1,5 @@ +import itertools + from enum import Enum from sentry_sdk._types import TYPE_CHECKING @@ -479,6 +481,7 @@ class ClientConstructor: def __init__( self, dsn=None, # type: Optional[str] + *, max_breadcrumbs=DEFAULT_MAX_BREADCRUMBS, # type: int release=None, # type: Optional[str] environment=None, # type: Optional[str] @@ -540,7 +543,7 @@ def __init__( def _get_default_options(): - # type: () -> Dict[str, Any] + # type: () -> dict[str, Any] import inspect if hasattr(inspect, "getfullargspec"): @@ -550,7 +553,14 @@ def _get_default_options(): a = getargspec(ClientConstructor.__init__) defaults = a.defaults or () - return dict(zip(a.args[-len(defaults) :], defaults)) + kwonlydefaults = a.kwonlydefaults or {} + + return dict( + itertools.chain( + zip(a.args[-len(defaults) :], defaults), + kwonlydefaults.items(), + ) + ) DEFAULT_OPTIONS = _get_default_options() diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 8e114a7de4..81abff8b5c 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -89,7 +89,7 @@ def _init(*args, **kwargs): This takes the same arguments as the client constructor. """ - client = Client(*args, **kwargs) # type: ignore + client = Client(*args, **kwargs) Scope.get_global_scope().set_client(client) _check_python_deprecations() rv = _InitGuard(client) From ae034ab82aef4e00d63e28e4465cb6aa9f6f8191 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:25:10 +0200 Subject: [PATCH 14/19] ref(consts): Remove Python 2 compatibility code (#3284) All the versions we now support include `inspect.getfullargspec`, so we no longer need the backwards-compatible fallback. --- sentry_sdk/consts.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index f03b263162..63b402d040 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -546,12 +546,7 @@ def _get_default_options(): # type: () -> dict[str, Any] import inspect - if hasattr(inspect, "getfullargspec"): - getargspec = inspect.getfullargspec - else: - getargspec = inspect.getargspec # type: ignore - - a = getargspec(ClientConstructor.__init__) + a = inspect.getfullargspec(ClientConstructor.__init__) defaults = a.defaults or () kwonlydefaults = a.kwonlydefaults or {} From 301c4b8a0654b2795a914b247422dfe649176ae9 Mon Sep 17 00:00:00 2001 From: colin-sentry <161344340+colin-sentry@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:19:01 -0400 Subject: [PATCH 15/19] OpenAI: Lazy initialize tiktoken to avoid http at import time (#3287) --- sentry_sdk/integrations/openai.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index b2c9500026..052d65f7a6 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -32,10 +32,13 @@ try: import tiktoken # type: ignore - enc = tiktoken.get_encoding("cl100k_base") + enc = None # lazy initialize def count_tokens(s): # type: (str) -> int + global enc + if enc is None: + enc = tiktoken.get_encoding("cl100k_base") return len(enc.encode_ordinary(s)) logger.debug("[OpenAI] using tiktoken to count tokens") From 84a2afcce4c3331e75a89506375d3f11de4c1634 Mon Sep 17 00:00:00 2001 From: Ash <0Calories@users.noreply.github.com> Date: Mon, 15 Jul 2024 02:58:29 -0400 Subject: [PATCH 16/19] feat(pymongo): Send query description as valid JSON (#3291) MongoDB queries were being sent as invalid JSON, since the keys and values were surrounded by single quotes instead of double quotes. Relay cannot parse the queries unless they are sent as valid JSON. This PR converts MongoDB queries into a JSON string before sending it on the span, so that Relay may properly parse it and extract metrics. --- sentry_sdk/integrations/pymongo.py | 3 ++- tests/integrations/pymongo/test_pymongo.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/pymongo.py b/sentry_sdk/integrations/pymongo.py index e81aa2d3b2..47fdfa6744 100644 --- a/sentry_sdk/integrations/pymongo.py +++ b/sentry_sdk/integrations/pymongo.py @@ -1,4 +1,5 @@ import copy +import json import sentry_sdk from sentry_sdk.consts import SPANSTATUS, SPANDATA, OP @@ -154,7 +155,7 @@ def started(self, event): if not should_send_default_pii(): command = _strip_pii(command) - query = "{}".format(command) + query = json.dumps(command, default=str) span = sentry_sdk.start_span( op=OP.DB, description=query, diff --git a/tests/integrations/pymongo/test_pymongo.py b/tests/integrations/pymongo/test_pymongo.py index be70a4f444..172668619b 100644 --- a/tests/integrations/pymongo/test_pymongo.py +++ b/tests/integrations/pymongo/test_pymongo.py @@ -71,9 +71,9 @@ def test_transactions(sentry_init, capture_events, mongo_server, with_pii): assert insert_success["tags"]["db.operation"] == "insert" assert insert_fail["tags"]["db.operation"] == "insert" - assert find["description"].startswith("{'find") - assert insert_success["description"].startswith("{'insert") - assert insert_fail["description"].startswith("{'insert") + assert find["description"].startswith('{"find') + assert insert_success["description"].startswith('{"insert') + assert insert_fail["description"].startswith('{"insert') assert find["tags"][SPANDATA.DB_MONGODB_COLLECTION] == "test_collection" assert insert_success["tags"][SPANDATA.DB_MONGODB_COLLECTION] == "test_collection" @@ -117,7 +117,7 @@ def test_breadcrumbs(sentry_init, capture_events, mongo_server, with_pii): (crumb,) = event["breadcrumbs"]["values"] assert crumb["category"] == "query" - assert crumb["message"].startswith("{'find") + assert crumb["message"].startswith('{"find') if with_pii: assert "1" in crumb["message"] else: From 5bad5c67f4953f1b9ada90904944ce4d9e9ab948 Mon Sep 17 00:00:00 2001 From: colin-sentry <161344340+colin-sentry@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:59:04 -0400 Subject: [PATCH 17/19] feat(openai): Make tiktoken encoding name configurable + tiktoken usage opt-in (#3289) Make tiktoken encoding name configurable + tiktoken usage opt-in --------- Co-authored-by: Ivana Kellyer --- sentry_sdk/integrations/langchain.py | 55 ++++++++---------- sentry_sdk/integrations/openai.py | 57 ++++++++----------- .../integrations/langchain/test_langchain.py | 16 +++++- tests/integrations/openai/test_openai.py | 16 +++++- 4 files changed, 80 insertions(+), 64 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 305b445b2e..60c791fa12 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -27,28 +27,6 @@ raise DidNotEnable("langchain not installed") -try: - import tiktoken # type: ignore - - enc = tiktoken.get_encoding("cl100k_base") - - def count_tokens(s): - # type: (str) -> int - return len(enc.encode_ordinary(s)) - - logger.debug("[langchain] using tiktoken to count tokens") -except ImportError: - logger.info( - "The Sentry Python SDK requires 'tiktoken' in order to measure token usage from streaming langchain calls." - "Please install 'tiktoken' if you aren't receiving accurate token usage in Sentry." - "See https://docs.sentry.io/platforms/python/integrations/langchain/ for more information." - ) - - def count_tokens(s): - # type: (str) -> int - return 1 - - DATA_FIELDS = { "temperature": SPANDATA.AI_TEMPERATURE, "top_p": SPANDATA.AI_TOP_P, @@ -78,10 +56,13 @@ class LangchainIntegration(Integration): # The most number of spans (e.g., LLM calls) that can be processed at the same time. max_spans = 1024 - def __init__(self, include_prompts=True, max_spans=1024): - # type: (LangchainIntegration, bool, int) -> None + def __init__( + self, include_prompts=True, max_spans=1024, tiktoken_encoding_name=None + ): + # type: (LangchainIntegration, bool, int, Optional[str]) -> None self.include_prompts = include_prompts self.max_spans = max_spans + self.tiktoken_encoding_name = tiktoken_encoding_name @staticmethod def setup_once(): @@ -109,11 +90,23 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] max_span_map_size = 0 - def __init__(self, max_span_map_size, include_prompts): - # type: (int, bool) -> None + def __init__(self, max_span_map_size, include_prompts, tiktoken_encoding_name=None): + # type: (int, bool, Optional[str]) -> None self.max_span_map_size = max_span_map_size self.include_prompts = include_prompts + self.tiktoken_encoding = None + if tiktoken_encoding_name is not None: + import tiktoken # type: ignore + + self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) + + def count_tokens(self, s): + # type: (str) -> int + if self.tiktoken_encoding is not None: + return len(self.tiktoken_encoding.encode_ordinary(s)) + return 0 + def gc_span_map(self): # type: () -> None @@ -244,9 +237,9 @@ def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs): if not watched_span.no_collect_tokens: for list_ in messages: for message in list_: - self.span_map[run_id].num_prompt_tokens += count_tokens( + self.span_map[run_id].num_prompt_tokens += self.count_tokens( message.content - ) + count_tokens(message.type) + ) + self.count_tokens(message.type) def on_llm_new_token(self, token, *, run_id, **kwargs): # type: (SentryLangchainCallback, str, UUID, Any) -> Any @@ -257,7 +250,7 @@ def on_llm_new_token(self, token, *, run_id, **kwargs): span_data = self.span_map[run_id] if not span_data or span_data.no_collect_tokens: return - span_data.num_completion_tokens += count_tokens(token) + span_data.num_completion_tokens += self.count_tokens(token) def on_llm_end(self, response, *, run_id, **kwargs): # type: (SentryLangchainCallback, LLMResult, UUID, Any) -> Any @@ -461,7 +454,9 @@ def new_configure(*args, **kwargs): if not already_added: new_callbacks.append( SentryLangchainCallback( - integration.max_spans, integration.include_prompts + integration.max_spans, + integration.include_prompts, + integration.tiktoken_encoding_name, ) ) return f(*args, **kwargs) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 052d65f7a6..d06c188712 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -14,7 +14,6 @@ from sentry_sdk.scope import should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.utils import ( - logger, capture_internal_exceptions, event_from_exception, ensure_integration_enabled, @@ -29,45 +28,33 @@ except ImportError: raise DidNotEnable("OpenAI not installed") -try: - import tiktoken # type: ignore - - enc = None # lazy initialize - - def count_tokens(s): - # type: (str) -> int - global enc - if enc is None: - enc = tiktoken.get_encoding("cl100k_base") - return len(enc.encode_ordinary(s)) - - logger.debug("[OpenAI] using tiktoken to count tokens") -except ImportError: - logger.info( - "The Sentry Python SDK requires 'tiktoken' in order to measure token usage from some OpenAI APIs" - "Please install 'tiktoken' if you aren't receiving token usage in Sentry." - "See https://docs.sentry.io/platforms/python/integrations/openai/ for more information." - ) - - def count_tokens(s): - # type: (str) -> int - return 0 - class OpenAIIntegration(Integration): identifier = "openai" origin = f"auto.ai.{identifier}" - def __init__(self, include_prompts=True): - # type: (OpenAIIntegration, bool) -> None + def __init__(self, include_prompts=True, tiktoken_encoding_name=None): + # type: (OpenAIIntegration, bool, Optional[str]) -> None self.include_prompts = include_prompts + self.tiktoken_encoding = None + if tiktoken_encoding_name is not None: + import tiktoken # type: ignore + + self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name) + @staticmethod def setup_once(): # type: () -> None Completions.create = _wrap_chat_completion_create(Completions.create) Embeddings.create = _wrap_embeddings_create(Embeddings.create) + def count_tokens(self, s): + # type: (OpenAIIntegration, str) -> int + if self.tiktoken_encoding is not None: + return len(self.tiktoken_encoding.encode_ordinary(s)) + return 0 + def _capture_exception(exc): # type: (Any) -> None @@ -80,9 +67,9 @@ def _capture_exception(exc): def _calculate_chat_completion_usage( - messages, response, span, streaming_message_responses=None + messages, response, span, streaming_message_responses, count_tokens ): - # type: (Iterable[ChatCompletionMessageParam], Any, Span, Optional[List[str]]) -> None + # type: (Iterable[ChatCompletionMessageParam], Any, Span, Optional[List[str]], Callable[..., Any]) -> None completion_tokens = 0 # type: Optional[int] prompt_tokens = 0 # type: Optional[int] total_tokens = 0 # type: Optional[int] @@ -173,7 +160,9 @@ def new_chat_completion(*args, **kwargs): "ai.responses", list(map(lambda x: x.message, res.choices)), ) - _calculate_chat_completion_usage(messages, res, span) + _calculate_chat_completion_usage( + messages, res, span, None, integration.count_tokens + ) span.__exit__(None, None, None) elif hasattr(res, "_iterator"): data_buf: list[list[str]] = [] # one for each choice @@ -208,7 +197,11 @@ def new_iterator(): span, SPANDATA.AI_RESPONSES, all_responses ) _calculate_chat_completion_usage( - messages, res, span, all_responses + messages, + res, + span, + all_responses, + integration.count_tokens, ) span.__exit__(None, None, None) @@ -266,7 +259,7 @@ def new_embeddings_create(*args, **kwargs): total_tokens = response.usage.total_tokens if prompt_tokens == 0: - prompt_tokens = count_tokens(kwargs["input"] or "") + prompt_tokens = integration.count_tokens(kwargs["input"] or "") record_token_usage(span, prompt_tokens, None, total_tokens or prompt_tokens) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 5e7ebbbf1d..b9e5705b88 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -46,6 +46,15 @@ def _llm_type(self) -> str: return llm_type +def tiktoken_encoding_if_installed(): + try: + import tiktoken # type: ignore # noqa # pylint: disable=unused-import + + return "cl100k_base" + except ImportError: + return None + + @pytest.mark.parametrize( "send_default_pii, include_prompts, use_unknown_llm_type", [ @@ -62,7 +71,12 @@ def test_langchain_agent( llm_type = "acme-llm" if use_unknown_llm_type else "openai-chat" sentry_init( - integrations=[LangchainIntegration(include_prompts=include_prompts)], + integrations=[ + LangchainIntegration( + include_prompts=include_prompts, + tiktoken_encoding_name=tiktoken_encoding_if_installed(), + ) + ], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 9cd8761fd6..b0ffc9e768 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -78,6 +78,15 @@ def test_nonstreaming_chat_completion( assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 +def tiktoken_encoding_if_installed(): + try: + import tiktoken # type: ignore # noqa # pylint: disable=unused-import + + return "cl100k_base" + except ImportError: + return None + + # noinspection PyTypeChecker @pytest.mark.parametrize( "send_default_pii, include_prompts", @@ -87,7 +96,12 @@ def test_streaming_chat_completion( sentry_init, capture_events, send_default_pii, include_prompts ): sentry_init( - integrations=[OpenAIIntegration(include_prompts=include_prompts)], + integrations=[ + OpenAIIntegration( + include_prompts=include_prompts, + tiktoken_encoding_name=tiktoken_encoding_if_installed(), + ) + ], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) From c45640b5e63cb60d8cc4ff8074459c7d1abeffe0 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 15 Jul 2024 10:02:17 +0000 Subject: [PATCH 18/19] release: 2.10.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ef926b32..aabfbb8557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 2.10.0 + +### Various fixes & improvements + +- feat(openai): Make tiktoken encoding name configurable + tiktoken usage opt-in (#3289) by @colin-sentry +- feat(pymongo): Send query description as valid JSON (#3291) by @0Calories +- OpenAI: Lazy initialize tiktoken to avoid http at import time (#3287) by @colin-sentry +- ref(consts): Remove Python 2 compatibility code (#3284) by @szokeasaurusrex +- docs(init): Fix `sentry_sdk.init` type hint (#3283) by @szokeasaurusrex +- Add the client cert and key support to HttpTransport (#3258) by @grammy-jiang +- ref(profiling): Deprecate `hub` in `Profile` (#3270) by @szokeasaurusrex +- ref(init): Stop using `Hub` in `init` (#3275) by @szokeasaurusrex +- ref(hub): Delete `_should_send_default_pii` (#3274) by @szokeasaurusrex +- test: Remove `Hub` usage in `conftest` (#3273) by @szokeasaurusrex +- ref(debug): Rename debug logging filter (#3260) by @szokeasaurusrex +- ref(tracing): Update `NoOpSpan.finish` signature (#3267) by @szokeasaurusrex +- ref(tracing): Remove `Hub` in `Transaction.finish` (#3267) by @szokeasaurusrex +- ref: Remove Hub from `capture_internal_exception` logic (#3264) by @szokeasaurusrex +- ref(scope): Improve `Scope._capture_internal_exception` type hint (#3264) by @szokeasaurusrex +- ref(types): Correct `ExcInfo` type (#3266) by @szokeasaurusrex +- ref: Stop using `Hub` in `tracing_utils` (#3269) by @szokeasaurusrex + ## 2.9.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index c63bee4665..ed2fe5b452 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.9.0" +release = "2.10.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 63b402d040..b4d30cd24a 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -562,4 +562,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.9.0" +VERSION = "2.10.0" diff --git a/setup.py b/setup.py index 0d412627b5..f419737d36 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.9.0", + version="2.10.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From b026dbd9b4eb74d51abc44ba7dc69e2fbcbf3892 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 15 Jul 2024 12:21:08 +0200 Subject: [PATCH 19/19] Update CHANGELOG.md --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aabfbb8557..8d6050b50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,23 +4,40 @@ ### Various fixes & improvements -- feat(openai): Make tiktoken encoding name configurable + tiktoken usage opt-in (#3289) by @colin-sentry -- feat(pymongo): Send query description as valid JSON (#3291) by @0Calories +- Add client cert and key support to `HttpTransport` (#3258) by @grammy-jiang + + Add `cert_file` and `key_file` to your `sentry_sdk.init` to use a custom client cert and key. Alternatively, the environment variables `CLIENT_CERT_FILE` and `CLIENT_KEY_FILE` can be used as well. + - OpenAI: Lazy initialize tiktoken to avoid http at import time (#3287) by @colin-sentry -- ref(consts): Remove Python 2 compatibility code (#3284) by @szokeasaurusrex -- docs(init): Fix `sentry_sdk.init` type hint (#3283) by @szokeasaurusrex -- Add the client cert and key support to HttpTransport (#3258) by @grammy-jiang -- ref(profiling): Deprecate `hub` in `Profile` (#3270) by @szokeasaurusrex -- ref(init): Stop using `Hub` in `init` (#3275) by @szokeasaurusrex -- ref(hub): Delete `_should_send_default_pii` (#3274) by @szokeasaurusrex -- test: Remove `Hub` usage in `conftest` (#3273) by @szokeasaurusrex -- ref(debug): Rename debug logging filter (#3260) by @szokeasaurusrex -- ref(tracing): Update `NoOpSpan.finish` signature (#3267) by @szokeasaurusrex -- ref(tracing): Remove `Hub` in `Transaction.finish` (#3267) by @szokeasaurusrex -- ref: Remove Hub from `capture_internal_exception` logic (#3264) by @szokeasaurusrex -- ref(scope): Improve `Scope._capture_internal_exception` type hint (#3264) by @szokeasaurusrex -- ref(types): Correct `ExcInfo` type (#3266) by @szokeasaurusrex -- ref: Stop using `Hub` in `tracing_utils` (#3269) by @szokeasaurusrex +- OpenAI, Langchain: Make tiktoken encoding name configurable + tiktoken usage opt-in (#3289) by @colin-sentry + + Fixed a bug where having certain packages installed along the Sentry SDK caused an HTTP request to be made to OpenAI infrastructure when the Sentry SDK was initialized. The request was made when the `tiktoken` package and at least one of the `openai` or `langchain` packages were installed. + + The request was fetching a `tiktoken` encoding in order to correctly measure token usage in some OpenAI and Langchain calls. This behavior is now opt-in. The choice of encoding to use was made configurable as well. To opt in, set the `tiktoken_encoding_name` parameter in the OpenAPI or Langchain integration. + + ```python + sentry_sdk.init( + integrations=[ + OpenAIIntegration(tiktoken_encoding_name="cl100k_base"), + LangchainIntegration(tiktoken_encoding_name="cl100k_base"), + ], + ) + ``` + +- PyMongo: Send query description as valid JSON (#3291) by @0Calories +- Remove Python 2 compatibility code (#3284) by @szokeasaurusrex +- Fix `sentry_sdk.init` type hint (#3283) by @szokeasaurusrex +- Deprecate `hub` in `Profile` (#3270) by @szokeasaurusrex +- Stop using `Hub` in `init` (#3275) by @szokeasaurusrex +- Delete `_should_send_default_pii` (#3274) by @szokeasaurusrex +- Remove `Hub` usage in `conftest` (#3273) by @szokeasaurusrex +- Rename debug logging filter (#3260) by @szokeasaurusrex +- Update `NoOpSpan.finish` signature (#3267) by @szokeasaurusrex +- Remove `Hub` in `Transaction.finish` (#3267) by @szokeasaurusrex +- Remove Hub from `capture_internal_exception` logic (#3264) by @szokeasaurusrex +- Improve `Scope._capture_internal_exception` type hint (#3264) by @szokeasaurusrex +- Correct `ExcInfo` type (#3266) by @szokeasaurusrex +- Stop using `Hub` in `tracing_utils` (#3269) by @szokeasaurusrex ## 2.9.0